乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

プログラミング

夏休みの時期ですねー
夏休みの課題は決まりましたか?

今回は2重振り子のシミュレーションです


解説動画はこちら



2重振り子のシミュレーション


スクリーンショット 2025-07-26 16.28.53
こんな感じの2重振り子
よく有りますよね

支点があって
2個の振り子がついてて
ブルンブルンしちゃうやつです。

今回はこれをPythonで計算して
動画にするやつです。



ルンゲ・クッタ法

2重振り子をシミュレーションするには
2点の位置

2つのx,y軸の座標を求める必要があります。

この計算を行うために使用するのが
ルンゲ・クッタ法というものです。


4次のルンゲ・クッタ法(Runge-Kutta 4次法)

微分方程式の数値解法の一つで
特に4次までを考慮したテイラー展開を用いることで
より精度の高い近似解を求める方法だそうです。


この方法では、変化率の推定値を4通り計算し
それらを加重平均することで
次のステップの値を計算することになります。

計算手順は以下のようになります。

k1 → 現在の変化率
k2 → 半歩進んだ時点の変化率(k1からの推定)
k3 → さらに半歩進んだ時点の変化率(k2からの推定)
k4 → 1ステップ進んだ時点の変化率

これらを 1:2:2:1 の比率で足し合わせて
現在の状態を更新します。

𝑑𝑡 / 6 * (𝑘1 + 2*𝑘2 + 2*𝑘3 + 𝑘4) 

4次のルンゲ・クッタ法は、傾きを4回サンプリングし
テイラー展開の4次項まで考慮した近似になるため、
長時間シミュレーションでも精度が高いみたいです。

早速これをコードに落とし込んでいきましょう。


シミュレーションコード

Google colabで実行できるコードになっているので
コピペして実行することができると思います。

まずはライブラリのインポートです。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import warnings
warnings.filterwarnings('ignore')


次にパラメータ設定と
数値計算部分のコードです。
# パラメータ設定
g = 9.81   # 重力加速度
L1, L2 = 1.0, 1.0  # 振り子の長さ
m1, m2 = 1.0, 1.0  # 振り子の質量

# 初期条件(角度と角速度)
theta1 = np.pi/2
theta2 = np.pi/2 + 0.1
omega1 = 0.0
omega2 = 0.0
state = np.array([theta1, omega1, theta2, omega2])

# 時間設定
dt = 0.05
t_max = 25
steps = int(t_max/dt)

# 保存用リスト
x1_list, y1_list = [], []
x2_list, y2_list = [], []

# 二重振り子の運動方程式
def derivatives(state):
    theta1, omega1, theta2, omega2 = state
    delta = theta2 - theta1
    denom1 = (m1 + m2) * L1 - m2 * L1 * np.cos(delta)**2
    denom2 = (L2/L1) * denom1
    a1 = (m2*L1*omega1**2*np.sin(delta)*np.cos(delta) +
          m2*g*np.sin(theta2)*np.cos(delta) +
          m2*L2*omega2**2*np.sin(delta) -
          (m1+m2)*g*np.sin(theta1)) / denom1
    a2 = (-m2*L2*omega2**2*np.sin(delta)*np.cos(delta) +
          (m1+m2)*(g*np.sin(theta1)*np.cos(delta) -
                   L1*omega1**2*np.sin(delta) -
                   g*np.sin(theta2))) / denom2
    return np.array([omega1, a1, omega2, a2])

# Runge-Kutta 4次法で数値計算
for _ in range(steps):
    k1 = derivatives(state)
    k2 = derivatives(state + dt*k1/2)
    k3 = derivatives(state + dt*k2/2)
    k4 = derivatives(state + dt*k3)
    state += (dt/6)*(k1 + 2*k2 + 2*k3 + k4)
    theta1, omega1, theta2, omega2 = state

    # 座標変換
    x1 = L1 * np.sin(theta1)
    y1 = -L1 * np.cos(theta1)
    x2 = x1 + L2 * np.sin(theta2)
    y2 = y1 - L2 * np.cos(theta2)
    x1_list.append(x1)
    y1_list.append(y1)
    x2_list.append(x2)
    y2_list.append(y2)
for文のところがルンゲクッタ法の部分です。
ここでk1 - k4までを計算し
次のステップに状態を更新しています。

最後に描画用に 2点のx,y 軸としてデータ化します。

動画の生成部分です。
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(-2.2, 2.2)
ax.set_ylim(-2.2, 2.2)
ax.set_aspect('equal')
ax.axis("off")
line, = ax.plot([], [], 'o-', lw=2)       # 振り子本体
trace, = ax.plot([], [], 'r-', alpha=0.5) # 軌跡

# 軌跡用リスト
trace_x, trace_y = [], []
def update(i):
    # 点の位置(始点 → 中点 → 先端)
    thisx = [0, x1_list[i], x2_list[i]]
    thisy = [0, y1_list[i], y2_list[i]]
    line.set_data(thisx, thisy)
    trace_x.append(x2_list[i])
    trace_y.append(y2_list[i])
    trace.set_data(trace_x, trace_y)
    return line, trace

ani = FuncAnimation(fig, update, frames=len(x1_list), interval=40, blit=True)
ani.save("double_pendulum.mp4", fps=30, dpi=150)
plt.close(fig)
matplotlibで描画を計算して
最終的にはmp4にします。


ファイル置き場にできた動画を見てみると
from IPython.display import Video
Video("double_pendulum.mp4", embed=True)
スクリーンショット 2025-07-26 16.50.32
こんな感じで軌跡が赤く残るような
動画が作成できます。

なかなか面白いので
長い時間で作ってみるのも
面白いかもしれません。

頑張れば振り子の数を増やしたり
色々なエフェクトを加えたりして
より面白くすることもできるかもしれません。

夏休みの課題が無くて困っているご家庭は
ぜひ面白い改造に挑戦してみてください。

今日はここまでです
それでは。

テレビの選挙特番などで
「当確」って出てきますよね。
あれの推定方法などについてです。

解説動画はこちら



テレビ番組によっては開票が行われてすぐに
当確が出てしまったりするケースがあります。

なぜこんなにも早く当確が出るのか
選挙の出口予想の仕組みについて解説します。




出口調査と区間推定の仕組み

選挙の当確を出すためには、それぞれの候補者の得票率を
「だいたい、ここからこの位までの区間に入ってるんじゃないか?」
という区間推定を行なっています。

出口調査で得られた、ある候補者の得票率を p とすると

その区間は次のような計算式で求められます。
スクリーンショット 2025-07-19 15.20.10
ここで必要になってくるのは
出口調査に必要な人数( n ) です。

この区間の誤差を少なくするためには
ある程度の人数が必要で、一般的には無作為に選ばれた
400人ほどが必要になってきます。




区間推定の計算

たとえばある選挙において
候補者 A , B の二人がいるとして
出口調査400人の結果が 
A : 220人
B : 180人
だったとします。

この際のA候補の得票率は

p = 220/400 = 0.55 

全体での得票率の区間の推定は
スクリーンショット 2025-07-19 17.52.43

50.12% ~ 59.88%
となります。

得票率の下限が50%を上回っているため
このまま行けば勝ちが見えてきます。



これが400人中210人だった場合はどうでしょうか

推定得票率: 52.50%
95% 信頼区間: 47.61% ~ 57.39%

これだと得票率の下限が50%を下回っているので
まだ決着がつけられません

もう少しサンプル数が増えた場合はどうなるでしょうか

今度は出口調査で4000人中2100人としてみます。
推定得票率: 52.50%
95% 信頼区間: 50.95% ~ 54.05%

n 調査人数が増えるほど誤差が少なくなり
推定された区間は短くなります。




実際には当確を決めるには

実際には候補者も多く、出口調査のみでは
正確には決まらないことが多いです。


スクリーンショット 2025-07-19 15.52.41

という計算式で当確ラインがもとまります。

これを用いて開票率が進むにつれ
区間推定の下限が当確ラインを超える得票率が獲得できている場合
当確が出せるということになります。






区間推定で当確を計算するコード

候補者が2人
開票が進んで、得票数と全体の数が分かったとします。
関数の引数に入力すると、当確結果がわかります。

最初は全体400 , 獲得210票とします。
import math

def check_win(candidate_votes, total_samples, confidence=0.99):
    if total_samples == 0:
        print("❌ 標本数が0のため、判定できません。")
        return

    # 推定得票率
    phat = candidate_votes / total_samples
    
    # z値の選択
    z = 1.96 if confidence == 0.95 else 2.58 if confidence == 0.99 else 1.64
    
    # 標準誤差と信頼区間
    se = math.sqrt(phat * (1 - phat) / total_samples)
    lower = phat - z * se
    upper = phat + z * se
    
    # 当確ラインの計算
    win_threshold = (1 + math.sqrt(z**2 / (z**2 + total_samples))) / 2

    # 表示
    print(f"推定得票率 : {phat:.2%}")
    print(f"{int(confidence * 100)}% 信頼区間 : {lower:.2%} ~ {upper:.2%}")
    print(f"当確ライン(開票数={total_samples}) : {win_threshold:.2%}")
    
    # 当確判定
    if lower >= win_threshold:
        print("OK : 候補者は『当確』と判断できます。")
    else:
        print("X :  まだ『当確』とは言えません。")

# 使用例
check_win(candidate_votes=210, total_samples=400, confidence=0.95)
推定得票率 : 52.50%
95% 信頼区間 : 47.61% ~ 57.39%
当確ライン(開票数=400) : 54.88%
X :  まだ『当確』とは言えません。

信頼区間の下限は
当確ラインを上回らないので、まだ当確出ません。


2100 , 4000 でやってみると

推定得票率 : 52.50%
95% 信頼区間 : 50.95% ~ 54.05%
当確ライン(開票数=4000) : 51.55%
X :  まだ『当確』とは言えません。

少し、区間が狭まりましたが
信頼区間の下限は
当確ラインを上回らないので、まだ当確出ません。


21000 , 40000 でやってみると

推定得票率 : 52.50%
95% 信頼区間 : 52.01% ~ 52.99%
当確ライン(開票数=40000) : 50.49%
OK : 候補者は『当確』と判断できます。


ようやく上回りました
これでようやく当確が出せるようになります。


出口調査で最初から大差がついている場合は
開票前にすでに決着がついている場合もあるようです。


今回は選挙の当確や
出口調査の仕組みについてでした。

こういった統計を用いた計算なんかも
Pythonを用いると簡単に計算できますね

選挙以外にも使えるので
覚えておくと仕事の幅が広がって便利です。

それでは。


今回はブラック-ショールズ方程式で
オプション価格を計算する方法についてです


解説動画はこちら




ブラック-ショールズ方程式とは

デリバティブの価格づけに現れる
偏微分方程式のことです。

デリバティブは株式や債券、通貨、商品などの
原資産から派生した金融商品の総称のことです。

ブラックショールズ方程式は主に
ヨーロピアン・オプション(満期時のみ行使可) の
理論価格を求めるためのモデルになっています。


オプション取引とは

金融商品のデリバティブの一種で
ある原資産について、あらかじめ決められた
将来の一定の日または期間において
事前に定めた権利行使価格で取引できる
「権利」のことです

原資産を
買う権利についてのオプションを「コールオプション」
売る権利についてのオプションを「プットオプション」
と呼んでいます。



日経225オプション

日経225オプションの価格情報があります。


オプション取引の価格計算ツール(日本取引所)

価格計算ツールもついているので
今回はこの計算方法をPythonで再現します。


ブラックショールズの計算式について

モデルの前提条件

•    価格は幾何ブラウン運動に従う
•    市場は完全で、裁定取引がない
•    無リスク金利は一定
•    ボラティリティ(変動率)は一定
•    配当は考慮しない(配当あり版も考慮は可)


使用する変数

S 現在の価格(Spot Price)
K 権利行使価格(Strike Price)
T 満期までの残存期間(年単位)
r 無リスク金利(年利)
σ 価格のボラティリティ(年率標準偏差)

ブラック-ショールズの数式

コールオプション価格(買う権利):
スクリーンショット 2025-07-12 17.06.54


プットオプション価格(売る権利):
スクリーンショット 2025-07-12 17.06.59

こんな感じの計算式になっていますが
途中、d1,d2というものが必要になります。

d_1, d_2 の定義
スクリーンショット 2025-07-12 17.08.28

なお、N(x) は 標準正規分布の累積分布関数(CDF)になります。


オプション計算のPythonコード

下記の関数で、オプション価格を計算できます。
import numpy as np
from scipy.stats import norm

def black_scholes_option_price(S, K, T, r, sigma, option_type='call'):
    """
    ブラック-ショールズ方程式によるオプション価格の計算
    S: 現在の株価
    K: 権利行使価格
    T: 残存期間(年単位)
    r: 無リスク金利(年利)
    sigma: ボラティリティ(年率標準偏差)
    option_type: 'call' か 'put'
    """
    d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    if option_type == 'call':
        price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    elif option_type == 'put':
        price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
    else:
        raise ValueError("option_type must be 'call' or 'put'")
    return price

実際に値を入れて計算する場合はこのようになります。
日数で計算したい場合は年単位なのでうまく合わせます。
# パラメータ例
S = 39425       # 現在の価格
K = 40000       # 権利行使価格
T = 62/365      # 満期まで62日
r = 0.0         # 無リスク金利 0%
sigma = 0.1883  # ボラティリティ 18.83%

call_price = black_scholes_option_price(S, K, T, r, sigma, option_type='call')
put_price = black_scholes_option_price(S, K, T, r, sigma, option_type='put')

print(f"Call Option Price: {call_price:.4f}")
print(f"Put Option Price : {put_price:.4f}")
Call Option Price: 963.0367
Put Option Price : 1538.0367




まとめ

この計算結果をどう応用するかですが
オプション取引の戦略を決定するために使います。

市場価格 > 理論価格

オプションが「割高」
プットまたはコールを「売る(ショート)」

市場価格 < 理論価格

オプションが「割安」
プットまたはコールを「買う(ロング)」


ということになります。

この辺りを実際の売買手順フローに落とし込むと
① 必要データ収集(S, K, T, r, σ)
        ↓
② BSモデルで理論価格算出(call/put)
        ↓
③ 実際の市場価格と比較(割高・割安を判断)
        ↓
④ 戦略選定(買う/売る or 戦略組み合わせ)
        ↓
⑤ 証券口座で売買、モニタリング

という感じになるので
自動取引ができるんじゃ無いかと
画策しているところです。

今回はブラックショールズ方程式による
オプション価格の計算方法についてでした。

今回はここまでです
それでは。






今回は地震データの可視化についてです
最近地震が多いので、plotlyで可視化してみました。


解説動画はこちら








データの入手先


気象庁の地震データベース

コードを動かしてみたい方は
CSVがダウンロードできるようなので
手元にダウンロードしてみてください。



データの読み込み

こちらのコードはGoogle Colabで動くようになっています。
動かしたい場合は Colabの画面左メニューから
フォルダマークをクリックすると
ファイル置き場が見えるので、そこにCSVファイルを
ドラッグなどで配置します。

ファイルを読み込みするには下記のコードです。

# 必要なライブラリのインポート
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import numpy as np
import warnings
warnings.filterwarnings('ignore')

file_path = "/content/地震リスト.csv"
df = pd.read_csv(file_path)
df.head()

df変数にデータが読み込まれると思います。



データの整形


CSVそのままのデータでは
うまく可視化が行えないため
可視化用にデータを加工する必要があります。

下記のコードを実行すると
可視化用のカラムなどが追加されます。


# データの前処理
s_order = ['震度1', '震度2', '震度3', '震度4', '震度5弱', '震度5強', '震度6弱', '震度6強', '震度7']
target = "トカラ列島近海"

def preprocess_data(df):
    # 緯度・経度の変換(度分秒から度へ)
    def convert_coordinate(coord_str):
        # 例: "29°28.2′N" -> 29.47
        if '°' in coord_str and '′' in coord_str:
            parts = coord_str.replace('N', '').replace('E', '').replace('S', '').replace('W', '')
            degree_part = parts.split('°')[0]
            minute_part = parts.split('°')[1].replace('′', '')
            return float(degree_part) + float(minute_part) / 60
        return float(coord_str)
    
    df['緯度_数値'] = df['緯度'].apply(convert_coordinate)
    df['経度_数値'] = df['経度'].apply(convert_coordinate)
    
    # 深さの数値化
    df['深さ_数値'] = df['深さ'].str.replace(' km', '').astype(float)
    
    # 震度の数値化
    s_mapping = {
        '震度1': 1, '震度2': 2, '震度3': 3, '震度4': 4, '震度5弱': 5,
        '震度5強': 5.5, '震度6弱': 6, '震度6強': 6.5, '震度7': 7
    }
    df['最大震度_数値'] = df['最大震度'].map(s_mapping)
    df['最大震度'] = pd.Categorical(df['最大震度'], categories=s_order, ordered=True)
    return df

# データの前処理を実行
df = preprocess_data(df)
df = df[df["震央地名"]==target]
df.head()



時系列の可視化


最初は時系列で
どれだけ地震が発生しているのかを
見てみましょう

日別、震度別で時系列で地震の回数を
表示してみます。

# 時系列分析(日付別)
df['日付'] = pd.to_datetime(df['地震の発生日'])
daily_shindo_counts = df.groupby(['日付', '最大震度']).size().reset_index(name='地震回数')

# 震度の順序を維持
daily_shindo_counts['最大震度'] = pd.Categorical(
    daily_shindo_counts['最大震度'], 
    categories=s_order, 
    ordered=True
)

# 震度別横並び棒グラフ
fig_time = px.bar(
    daily_shindo_counts,
    x='日付',
    y='地震回数',
    color='最大震度',
    title='日付別地震発生回数(震度別)',
    labels={'地震回数': '地震発生回数', '日付': '発生日'},
    category_orders={'最大震度': s_order},
    barmode='group'  # 横並びに表示
)

# レイアウトの調整
fig_time.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000,
    legend=dict(
        orientation="v",
        yanchor="top",
        y=1,
        xanchor="left",
        x=1.01
    )
)

fig_time.show()
スクリーンショット 2025-07-05 17.27.01

データは直近の1週間分しか無いようです。
7月前後で急増しているのが分かります。



箱ひげ図の可視化


次は箱ひげ図で
「トカラ列島近海」の詳細を見てみましょう。


震度ごとにマグニチュードをまとめると
このような感じになります。

# 震央地名 最大震度別での箱ひげ図

fig_box = px.box(df, 
                 x='震央地名', 
                 y='M',
                 color='最大震度',
                 title='震央地名別 マグニチュード分布(最大震度別)',
                 labels={'M': 'マグニチュード', '震央地名': '震央地名'},
                 category_orders={'最大震度': s_order})

fig_box.update_layout(
    xaxis_tickangle=-45,
    height=600,
    width=1000
)
fig_box.show()
スクリーンショット 2025-07-05 17.29.52
同じ震度でも、マグニチュードにはバラツキがありますが
マグニチュードが上がるにつれ、震度も大きくなっています。



地図表示

今後は地図で震度を表示してみましょう
OpenStreetMapを使用して、緯度軽度を用いて
地図に震度を表示させてみます。

# マップ表示用のデータ準備
df_map = df.copy()
df_map['緯度_数値'] = pd.to_numeric(df_map['緯度_数値'], errors='coerce')
df_map['経度_数値'] = pd.to_numeric(df_map['経度_数値'], errors='coerce')
df_map['M'] = pd.to_numeric(df_map['M'], errors='coerce')
df_map = df_map.dropna(subset=['緯度_数値', '経度_数値', 'M'])
df_map['最大震度'] = pd.Categorical(df_map['最大震度'], categories=s_order, ordered=True)

# 緯度経度と最大震度・Mを用いたmap表示(OpenStreetMap使用)
fig_map2 = go.Figure()

# 震度別に色分けしてプロット
震度_colors = {
    '震度1': '#90EE90',  # 薄緑
    '震度2': '#FFD700',  # 金色
    '震度3': '#FFA500',  # オレンジ
    '震度4': '#FF6347',  # トマト色
    '震度5弱': '#FF4500',  # 赤オレンジ
    '震度5強': '#FF0000',  # 赤
    '震度6弱': '#8B0000',  # 濃い赤
    '震度6強': '#4B0082',  # インディゴ
    '震度7': '#000000'   # 黒
}

for 震度 in s_order:
    if 震度 in df_map['最大震度'].values:
        subset = df_map[df_map['最大震度'] == 震度]
        fig_map2.add_trace(go.Scattermapbox(
            lat=subset['緯度_数値'],
            lon=subset['経度_数値'],
            mode='markers',
            marker=dict(
                size=subset['最大震度_数値'] ** 2 ,  # サイズ調整
                color=震度_colors.get(震度, '#000000'),
                sizemode='diameter'
            ),
            text=subset['震央地名'],
            hovertemplate='%{text}
' + '緯度: %{lat:.2f}
' + '経度: %{lon:.2f}
' + 'マグニチュード: %{customdata[0]}
' + '最大震度: %{customdata[1]}
' + '', customdata=list(zip(subset['M'], subset['最大震度'])), name=震度 )) fig_map2.update_layout( title='地震発生位置(最大震度・マグニチュード)- OpenStreetMap', mapbox=dict( style="open-street-map", center=dict(lat=df_map['緯度_数値'].mean(), lon=df_map['経度_数値'].mean()), zoom=8 ), height=700, width=1000 ) fig_map2.show()
スクリーンショット 2025-07-05 17.34.15

今回の地震は、かなり局所的に起きていることが分かります。
特に震度5以上が発生した箇所がかなり近く
この近辺は注意が必要な地域に見えます。


散布図表示


最後に
マグニチュード、震度、深さを
散布図で見てみましょう

# 深さとマグニチュードの関係
fig_scatter = px.scatter(
    df,
    x='深さ_数値',
    y='M',
    color='最大震度',
    size='最大震度_数値',
    hover_name='震央地名',
    hover_data={
        '地震の発生日': True,
        '地震の発生時刻': True
    },
    title='深さとマグニチュードの関係(最大震度別)',
    labels={'深さ_数値': '深さ (km)', 'M': 'マグニチュード'},
    category_orders={'最大震度': s_order}
)

fig_scatter.show()
スクリーンショット 2025-07-05 17.36.27


どの深さでも、満遍なく起きているように見えますね
地震自体は、いつ何時、どこでも起きてしまいます。



まとめ

緯度経度が記載されているデータは
plotlyなどを用いれば地図表示が比較的
簡単に行うことができます。

可視化の表現の幅が広がるので
覚えておくと良いかもしれませんね


今日はここまでです
それでは




今回はドルコスト平均法による
積立投資のシミュレーションです

解説動画はこちら



ドルコスト平均法とは

価格が変動する金融商品に対して
一定金額を定期的に購入していく投資方法のことです

今回は毎月10万円積み立てるとして
それがどうなるかをシミュレーションしていきます。



eMaxisSlim米国株式S&P500

リンク先

S&P500指数(配当込み、円換算ベース)に
連動する運用成果を目指す投資信託
2018年7月3日に設定された商品です。

今回はこのデータを用いて
シミュレーションをしていきます。



eMAXIS Slim 米国株式 S&P500の価格推移

価格のデータを定義して
価格推移をだしてみます。

import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
import matplotlib.pyplot as plt

# データ定義
data = {
    2018 : [0,0,0,0,0,0,10038,10458,10709,11051,10188,10468],
    2019 : [8809,9854,10408,10565,10900,10031,10725,10978,10491,10890,11106,11680],
    2020 : [11873,11892,10825,9474,10655,11205,11465,11886,12707,12182,11767,12989],
    2021 : [13339,13407,14010,15217,15754,15924,16540,16712,17301,16677,18267,18005],
    2022 : [19291,18130,17601,19347,18798,18658,18052,19265,19394,18280,20283,19728],
    2023 : [17690,18714,19167,19388,20234,20666,22867,23260,23411,22911,22677,24155],
    2024 : [24154,25486,27473,28563,28573,29833,31693,29763,29789,29974,31336,32768],
    2025 : [33928,34065,32500,30512,28931,30867]
}

records = []
for year, prices in data.items():
    for month_idx, price in enumerate(prices):
        month = month_idx + 1
        if price == 0:
            continue
        date_str = f"{year}-{month:02d}-01"
        date = pd.to_datetime(date_str)
        records.append({"Date": date, "Price": price})

df = pd.DataFrame(records)
df = df.sort_values("Date")

# Plotlyでプロット
fig = px.line(df, x="Date", y="Price", title="Emaxis Slim value")
fig.show()
スクリーンショット 2025-06-21 16.43.01


良い感じに右肩上がりのようですが
この商品を毎月10万円ずつ積み立てたらどうなっていたでしょうか



2018年から2025年まで毎月積み立てた際のシミュレーション

毎月10万円
購入できる購入数量を算出して
2025年の最後の価格で
どれだけのリターンになっているかを算出します。
monthly_investment = 100000  # 毎月XX万円

# シミュレーション開始
total_units = 0  # 累計購入数量
total_invested = 0  # 累計投資金額
for year in sorted(data.keys()):
    for month_idx, price in enumerate(data[year], 1):
        if price == 0:
            continue
        units_bought = monthly_investment / price
        total_units += units_bought
        total_invested += monthly_investment

latest_year = max(data.keys())
latest_price = data[latest_year][-1]
current_value = total_units * latest_price
profit = current_value - total_invested
roi = (current_value / total_invested - 1) * 100

# 結果表示
print(f"総投資額: {total_invested:,.0f}円")
print(f"最終評価額: {current_value:,.0f}円")
print(f"損益: {profit:,.0f}円")
print(f"累計購入数量: {total_units:.4f}口")
print(f"リターン: {roi:.2f}%")
total_return = (100 + roi) / 100
years = 8
cagr = (total_return ** (1 / years)) - 1
print(f"年平均リターン: {cagr * 100:.2f}%")

総投資額: 8,400,000円
最終評価額: 16,458,895円
損益: 8,058,895円
累計購入数量: 533.2198口
リターン: 95.94%
年平均リターン: 8.77%


この価格推移のデータ上では
年平均8%超

7年で二倍近くの運用成績になっています。



ターンヒートマップ


今度は開始年から終了年までの
リターンヒートマップを出してみましょう。


開始と終了年を設定して
開始終了までのリターンを算出します。

years = list(data.keys())
monthly_invest = 100000
results = pd.DataFrame(index=years[:-1], columns=years[1:])
for start in years[:-1]:
    for end in years[years.index(start)+1:]:
        total_invest = 0
        total_units = 0
        for y in range(start, end+1):
            for price in data[y]:
                if price == 0:
                    continue
                units = monthly_invest / price
                total_units += units
                total_invest += monthly_invest
        # 最後の年の最後の価格で評価額計算
        final_price = [p for p in data[end] if p != 0][-1]
        final_value = total_units * final_price
        return_pct = (final_value - total_invest) / total_invest * 100
        results.loc[start, end] = return_pct

# ヒートマップ描画
results = results.astype(float)
plt.figure(figsize=(10, 7))
sns.heatmap(results, annot=True, fmt=".2f", cmap="coolwarm", center=0)
plt.title("dollar cost averaging method return heatmap (%)")
plt.xlabel("end year")
plt.ylabel("start year")
plt.show()
download

年単位だとどの年で買って売っても
プラスにはなっているようです。



まとめ

eMAXIS Slim 米国株式 S&P500のドルコスト平均法による積み立ては
年単位での運用であればマイナスは無いようです。

ただし、月単位だと乱高下があり
損する場合も有ったので、やはり長期積立で
見守るのが良いのではないかと思われます。

投資は自己判断で行うことが大切です。
その金融商品の良し悪しを判断し
投資するかどうかは人に言われるでなく
自分自身の判断で行うのが重要です。

投資判断を行うための材料作りとして
プログラムを用いたシミュレーションは
かなり役に立ちます。

データさえあればシミュレーションでき
結果の良し悪しから
投資判断の材料に使えるようになると思うので
プログラミングが出来るようになっていると
すぐに試すことができるのでお勧めです。

投資 x プログラミング
というテーマを今後も取り扱っていくので
両方できるようになりたい方は
要チェックしてみてください。

それでは


このページのトップヘ