配当再投資シミュレーターを作る(numpy × matplotlib)
Python経験者向けの投資分析シリーズ第12回。配当を再投資したときの資産推移を、入力した仮定から機械的に計算するCLIツールを作ります。np.fv は削除済みで動かない、年利を12で割ると答えが変わる、端株が買えないモデルにすると結果がずれる——「シミュレーターの数字」がどこから出てくるのかを、実行結果つきで分解します。
- 1. この記事のゴールと連載の位置づけ
- 2. シミュレーターは予測装置ではない — 前提を全部入力にする
- 3. 1年ぶんの計算を式にする — 配当・税・買い増し
- 4. numpyで書く — 消えた np.fv と、cumprod が使えない場所
- 5. つまずき実演①:年利を12で割ると、答えが変わる
- 6. つまずき実演②:丸めと桁は、静かに1円をずらす
- 7. つまずき実演③:端株は買えない — モデルを選んだ時点で答えが変わる
- 8. matplotlibで描く — 図の中に前提を焼き込む
- 9. 動かす — CLI・CSV・株価が下がる仮定・第8回DBの読み取り
- 10. 配当フェーズ5本の回収と、このツールで分からないこと
- 11. よくある質問(FAQ)
1. この記事のゴールと連載の位置づけ
本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第12回です。第11回の最後で予告した、配当を再投資したときの推移をシミュレーションするツールがここです。入力した仮定から、配当を再投資した場合としなかった場合の資産推移を計算するCLIツールを作ります。
本記事が作るのは「入力した仮定がそのまま続いたら、計算上どうなるか」だけを出す道具です。出てくる数字は予測でも見通しでも保証でもありません。利回り・増配率・株価上昇率・税率・年数はすべて利用者が入力する値で、コードに既定値は置いていません。記事とコードに登場する数値はすべて動作確認のための仮の値であり、推奨値ではありません。当サイトは投資助言業(金融商品取引業)の登録をしておらず、個別銘柄の推奨や売買タイミングの助言は行いません。
7つの前提を引数で受け取り、年ごとの株価・受取配当・買い増し株数・保有株数・評価額を表とCSVに出し、再投資あり/なしの2系列を図にするところまで。本文は要点のコードだけで、完成版は reinvest_sim.py(359行)にあります。実行結果は Windows 11 / Python 3.12.10 / numpy 2.4.6 / matplotlib 3.11.1 で2026年9月5日に確認したもので、乱数は使わないため同じ入力なら同じ結果が出ます。作り方だけ急ぐなら3章→4章→8章→9章で足ります。5〜7章は、そこで踏む地雷の先回りです。
配当フェーズ 先行4本との住み分け
第8回から続く配当フェーズは、ここが5本目で最後です。第8回で入金を記録し、第9回で実績を1年ぶんのカレンダーに並べ、第10回で候補を条件で絞り、第11回で優待の権利確定月を管理しました。本記事だけが「まだ起きていない年」を扱います。第9回が出すのは前年の実績を写した見込みで、実績というよりどころがある。本記事の入力は利用者が決めた仮定で、よりどころは何もありません。
このツールは外部と一切通信しません。株価も配当も取りに行かず、計算に使うのは引数で渡した値だけです。将来の利回りや増配率はどこかから取得できる種類のデータではないので、通信する理由がありません。第8回の dividends.db を読む --from-db もローカルのファイルを読み取り専用で開くだけで、1行も書き込みません(9章)。
2. シミュレーターは予測装置ではない — 前提を全部入力にする
この記事でいちばん強い設計判断を先に書きます。将来に関わる7つの引数に、既定値を1つも置きません。
# 将来に関わる7つの前提には default を置かない(既定値は推奨値に見えるため)
p.add_argument("--shares", type=int, required=True, help="初期の保有株数(株)")
p.add_argument("--price", type=float, required=True, help="初期の株価(円)")
p.add_argument("--dps", type=float, required=True, help="1株あたりの年間配当(円・税引前)")
p.add_argument("--dividend-growth", type=float, required=True,
help="1株配当が毎年変化する率(%・マイナス可)")
# --price-growth / --tax-rate / --years も同じく required=True(以下略)
--dividend-growth 5 のような値を書いておけば引数なしで動きます。ただしその数字は「とりあえずこのくらい」という見立てとして読まれます。増配率の見立てを配る道具にするつもりはないので、第10回でしきい値に既定値を置かなかったのと同じ理由で required=True にしました。引数を忘れると使い方を表示して終了コード2で止まります。
$ python reinvest_sim.py
usage: reinvest_sim.py [-h] --shares SHARES --price PRICE --dps DPS
--dividend-growth DIVIDEND_GROWTH --price-growth
PRICE_GROWTH --tax-rate TAX_RATE --years YEARS
(中略)
reinvest_sim.py: error: the following arguments are required: --shares, --price, --dps, --dividend-growth, --price-growth, --tax-rate, --years
こう設計すると、出力の責任が入力側にあることをコードの形で示せます。文章で「これは予測ではありません」と書くより、既定値が存在しないほうが強い。
このモデルが扱わないもの
このシミュレーターは現実の運用をかなり削り落とした簡略モデルです。何を無視したのかを先に並べます。
| 扱わないもの | 実際には何が起きるか/計算がどちらにずれるか |
|---|---|
| 売買コスト | 買付のたびに手数料や価格への上乗せが発生する場合がある(条件は口座ごとに違う)。計算は楽観側にずれる |
| 配当の入金時期 | 権利が確定してから入金まで2〜3か月かかる(第9回で確認)。再投資が現実より早く回る |
| 減配・無配 | 配当は会社の決定で減ることも止まることもある。増配率を正の値で固定すると楽観側 |
| 株価の変動 | 毎年同じ率で動くことはない。途中の上下が一切表現されない |
| 物価の変動 | 同じ金額の価値が年々変わる。出力は名目額のまま |
| 制度の改正 | 税率も口座制度も変わる(3章)。税率を固定した計算は前提が古びる |
グラフは前提を全部飲み込んで1本の線に見せてしまいます。線の滑らかさと現実の滑らかさは関係がありません。
3. 1年ぶんの計算を式にする — 配当・税・買い増し
先に1年ぶんの流れを言葉で固定します。保有株数 × 1株配当で税引前の配当が決まり、税を引いた残りが現金になり、その現金で買えるだけ株を買い、買えなかった端数は翌年に繰り越す。これだけです。金額はすべて円、株数は株で持ち、prices[y] は y年目末の株価、dps[y-1] は y年目に受け取る1株あたり配当とします。コードにすると、配布コードの simulate() の中心部分がそのまま対応します。
for y in range(1, years + 1):
gross[y] = shares[y - 1] * dps[y - 1]
net[y] = gross[y] * (1.0 - tax_rate)
purse = cash[y - 1] + net[y]
held = shares[y - 1]
if reinvest:
unit_cost = prices[y] * lot # 1回の買付に必要な金額
if unit_cost > 0:
units = np.floor(purse / unit_cost) # 端株は買えない
bought[y] = units * lot
purse -= bought[y] * prices[y]
held += bought[y]
shares[y] = held
cash[y] = purse
ここで押さえておきたいのは、買った瞬間に資産は1円も増えていないことです。現金が株に置き換わっただけなので、その年末の評価額は再投資してもしなくても同じ値になります。差が生まれるのは翌年以降で、増えた株数に次の配当が付くことと、株価が動くことの2つが効いてきます。株価が下がる仮定を入れると、この2つ目が逆向きに効きます(9章)。
税率は定数ではなくパラメータ
税率を 0.20315 とコードに埋め込みたくなりますが、埋め込みませんでした。制度が動くからです。
2026年9月時点の一次情報を確認します。上場株式等の配当等は「15.315パーセント(他に地方税5パーセント)の税率により所得税および復興特別所得税が源泉徴収されます」(国税庁 No.1330)。申告分離課税を選んだ場合は「20.315パーセント(所得税および復興特別所得税15.315パーセント、地方税5パーセント)」(No.1331)。この0.315%の内訳はNo.2507にあります(いずれも2026年9月5日確認)。
この内訳は2027年1月から変わります。国税庁のパンフレットは「防衛特別所得税が創設され」「復興特別所得税について、その税率が1.1%(改正前:2.1%)に引き下げられ、課税期間が令和29年12月31日まで(改正前:令和19年12月31日まで)10年間延長されました」と説明しています。適用は「令和9年1月1日以後に生ずる所得に対する所得税」からです(防衛特別所得税及び復興特別所得税の源泉徴収のあらまし・2026年9月5日確認)。同じPDFは「合計税率(2.1%)に変更はありませんので、改正前後で、源泉徴収税額の計算方法に変更は生じません」とも書いています。
タックスアンサーNo.1330には復興特別所得税の期間として「令和19年12月31日まで」(=2037年)と書かれていますが、上のPDFのとおり課税期間は令和29年(2047年)まで延長済みです。2026年9月5日時点でNo.1330・No.1331・No.2507の本文はこの改正に触れていません。一次情報どうしが同じ更新速度で動くとは限らない、という実例です。税率を確認するときは必ず最新の国税庁の資料を見てください。
だからこのツールは --tax-rate で税率を外から受け取ります。税率は口座の種類や制度によっても変わります。NISA(少額投資非課税制度)の扱いは国税庁 No.1535と金融庁のNISA特設サイトで確認してください。本ツールは渡された税率を掛けるだけで、口座制度の判定は一切しません(--tax-rate 0 を渡せば税を引かない計算になります)。税務の適用は税理士か国税庁にご確認ください。
4. numpyで書く — 消えた np.fv と、cumprod が使えない場所
「Python 複利計算」で出てくる古い記事には、np.fv() を使ったコードが残っています。動きません。
>>> import numpy as np
>>> np.fv(0.05, 20, 0, -1_000_000)
AttributeError: module 'numpy' has no attribute 'fv'
これはバグではなく仕様変更です。NEP 32 は「Deprecate the financial functions in the numpy namespace, beginning in NumPy version 1.18. Remove the financial functions from NumPy version 1.20.」と書いていて、対象は fv・irr・npv・pmt など10関数。同じNEPが「Create a new Python package, numpy_financial」と、別パッケージへの分離も宣言しています。
numpy-financial を入れれば npf.fv() は使えます。本記事では入れません。この関数は「毎回同じ額を積み立てる」モデルの元利計算で、増配率・税率・端株といった本記事の主題を表現できないからです。
NumPy 2.0で名前が消えた関数もあります。
>>> np.cumproduct([1, 2, 3])
AttributeError: module 'numpy' has no attribute 'cumproduct'
np.cumproduct・np.product・np.round_・np.float_ はNumPy 2.0で削除され、それぞれ np.cumprod・np.prod・np.round・np.float64 に一本化されました。コピーしたコードが AttributeError で止まったら、まず名前が生き残っているかを疑うのが早いです。
cumprod で書ける系列と、書けない系列
株価と1株配当は「毎年同じ率を掛ける」だけなので、np.cumprod で一気に作れます(配布コードの growth_series())。
factors = np.full(count - 1, 1.0 + rate, dtype=np.float64)
return first * np.concatenate([[1.0], np.cumprod(factors)])
いっぽう保有株数は cumprod では書けません。今年買える株数は今年の現金額で決まり、その現金額は去年いくら余ったかで決まる。np.floor が途中に入るせいで、掛け算の累積では表せない形になっています。だから simulate() のループは残しました。ベクトル化できない処理を無理に畳むより、できない理由を書いてループを残すほうが読めます。
ただし再投資しない側は株数が動かないので、1000 * prices + np.concatenate([[0.0], np.cumsum(net)]) のようにループなしで書けます(net は1年目〜N年目の税引後配当。simulate() の戻り値なら res["net"][1:])。simulate() のループ版と突き合わせると、最大の差は0.0円でした。
ここで許容差を指定せず「allclose が True だから一致した」で済ませるのは危険です。np.isclose の既定は rtol=1e-5、判定式は |a - b| <= atol + rtol * |b| なので、金額が大きくなるほど許容差も比例して大きくなります。
>>> np.isclose(1e7, 1e7 + 100)
np.True_
>>> np.isclose(1e7, 1e7 + 101)
np.False_
1,000万円のスケールでは約100円までが「ほぼ等しい」判定になります。金額どうしを比べるなら rtol=0, atol=0.5 のように円単位で許容差を宣言するほうが、何を検証したのかがはっきりします。np.isclose は第2引数を基準にする非対称な関数でもあるので、引数の順番も意味を持ちます(NumPy公式)。
整数の cumprod は、条件が揃うと黙って壊れる
公式ドキュメントのNotesに「Arithmetic is modular when using integer types, and no error is raised on overflow.」とあります。ただし、実際に壊すには条件があります。
>>> arr = np.array([1000] * 6, dtype=np.int32)
>>> np.cumprod(arr)[-1] # dtype を指定しないとき
np.int64(1000000000000000000)
>>> np.cumprod(arr, dtype=np.int32) # dtype を明示したとき
array([ 1000, 1000000, 1000000000, -727379968, -1530494976,
-1486618624], dtype=int32)
dtype を省略するとint64に昇格して正しい値が返り、dtype=np.int32 と明示したときだけ警告なしで負の値になりました。「整数のcumprodは必ず壊れる」ではなく「幅の狭い整数型を自分で指定したときに静かに壊れる」が正確です。金額や株数を int32 に固定する理由はないので、本ツールは float64 で通しています。
この「エラーが出ないまま結果だけ変わる」罠は、6章の丸めと float32、8章の対数軸にも出ます。
5. つまずき実演①:年利を12で割ると、答えが変わる
「年利5%を月利にするなら 0.05 / 12」——これは間違いではありませんが、もう1つの答えがあります。
r = 0.05
nominal = r / 12 # 名目:単純に12で割る
effective = (1 + r) ** (1 / 12) - 1 # 実効:12回かけて元に戻る率
print(f"名目 r/12 = {nominal:.8f}")
print(f"実効 (1+r)**(1/12)-1 = {effective:.8f}")
print(f"12か月後の倍率: 名目 {(1 + nominal) ** 12:.8f} / 実効 {(1 + effective) ** 12:.8f}")
名目 r/12 = 0.00416667
実効 (1+r)**(1/12)-1 = 0.00407412
12か月後の倍率: 名目 1.05116190 / 実効 1.05000000
同じ「年利5%」から出発したのに、12か月後の倍率が 1.0512 と 1.0500 に割れます。どちらかが計算間違いなのではなく、「年利」という言葉が何を指すかが違うだけです。
どちらの読み方にも根拠があります。全国銀行協会の解説は「半年複利や一ヶ月複利などについても計算式の『利率』と『年数』を置き換えてみてください」と書いています(全国銀行協会・2026年9月5日確認)。ただし割り方までは示されていません。素直に読めば利率を回数で割る名目方式です。いっぽう米国の預金金利表示規則(Regulation DD)は年換算利回りを幾何的に年換算する実効方式で定義しています(12 CFR Part 1030 Appendix A)。
複利そのものの定義は「発生した利息を元本に足し、新しい元本として利息を計算する方法」(J-FLEC 用語集「複利」)で、ここに曖昧さはありません。曖昧なのは「1期をどれだけの長さにするか」のほうです。
配当は毎日たまるものではなく、年1回か2回、まとまって入ってくる離散的なできごとです。月次に割り振ると上の2方式のどちらかを選ぶ必要が出てきますが、年を1ステップにすればその選択自体が発生しません。月次で回したいなら、どちらの方式で割ったかを出力に書き添えてください。選んだ前提を隠さないことが、精度より先に来ます。
6. つまずき実演②:丸めと桁は、静かに1円をずらす
配当を円単位に丸めてから再投資すると、丸め方式の違いが結果に残ります。まず np.round の挙動から。公式ドキュメントは「For values exactly halfway between rounded decimal values, NumPy rounds to the nearest even value.」と明記しています(numpy.round)。いわゆる銀行家丸めです。
| 値 | np.round | 組み込み round() | Decimal+ROUND_HALF_UP |
|---|---|---|---|
| 0.5 | 0.0 | 0 | 1 |
| 1.5 | 2.0 | 2 | 2 |
| 2.5 | 2.0 | 2 | 3 |
| 3.5 | 4.0 | 4 | 4 |
numpyだけの話ではありません。組み込みの round() も同じ偶数丸めで、decimal の既定も ROUND_HALF_EVEN です。四捨五入がほしいなら rounding=ROUND_HALF_UP と方式を明示する必要があります(Python公式 decimal)。
「毎年丸める」と「最後に丸める」は同じ答えにならない
丸めをどこで行うかでも結果が変わります。100万円を年0.1%で16年回すという、変哲のない条件で試します。
rate, years, principal = 0.001, 16, 1_000_000
each = principal
for _ in range(years):
each = round(each * (1 + rate)) # 毎年、円未満を丸める
last = round(principal * (1 + rate) ** years) # 最後に一度だけ丸める
print("毎年丸め :", each)
print("最後に丸め:", last)
print("差(円) :", each - last)
毎年丸め : 1016120
最後に丸め: 1016121
差(円) : -1
1円です。金額としては小さいものの、同じ入力・同じ式で答えが2つあるという事実のほうが重要です。再投資のループでは、丸めた額が翌年の買付株数を変え、株数の差が次の配当を変えるので、差はこの1円で止まりません。だから配布コードは計算の途中では一切丸めず、表示とCSVに書き出す直前だけ丸めます(build_rows() のdocstringに「表示用の丸めはここだけで行う」と書いてあるのはそのためです)。
第8回では入金額を銭単位の整数で持ちました。あちらは「実際に入金された額」という動かない事実を記録する側なので、丸め方式を1つに固定できます。本記事は仮定を計算する側なので、浮動小数点で計算して表示だけ丸めるほうを選びました。
float32 にすると1円が消える
「配列だしメモリを節約しよう」と np.float32 を選ぶと、金額の計算では思わぬところで刻みが消えます。
>>> np.float32(16777216) + np.float32(1)
np.float32(1.6777216e+07)
>>> np.float32(16777217)
np.float32(1.6777216e+07)
>>> np.float64(16777217)
np.float64(16777217.0)
float32 の仮数部は24ビットなので、16,777,216(=2の24乗)を超えると刻みが2になり、1円単位が表現できなくなります。資産額が1,677万円あたりを超えると、奇数円が静かに消えるということです。float64 は仮数部が53ビットあるので、同じことが起きるのは9,007兆円あたり(=2の53乗)です。円建ての計算では気にする必要がありません。金額の配列は float64 で通す——配布コードが dtype=np.float64 を明示しているのはこのためです。
7. つまずき実演③:端株は買えない — モデルを選んだ時点で答えが変わる
配当額をそのまま株価で割れば「0.37株買った」ことにできます。計算としては素直ですが、そう決めた時点で答えは変わっています。
日本株の1単元の株式数は「1,000株を超えない範囲で会社が定款で自由に定めることができます」(J-FLEC 用語集「単元株」)とされ、100株という数字は2018年10月に売買単位が統一された結果です(J-FLEC 投資の時間 Q&A・2026年9月5日確認)。1株単位で売買できる仕組みも別にあり、どの単位で買えるかは口座と銘柄の条件しだいです。ツール側では決められません。
そこで --lot で買付単位を渡せるようにして、np.floor で切り捨てた分を現金として繰り越します(3章のコード)。この買付単位と税率の組み合わせで、同じ入力から3つの答えが出ます(--compare-models は --lot を見ません)。
$ python reinvest_sim.py --shares 1000 --price 2000 --dps 60 \
--dividend-growth 5 --price-growth 2 --tax-rate 20.315 --years 20 --compare-models
同じ入力・3つのモデル(相対差は税引前モデルを基準)
税引前で再投資(税を無視した楽観モデル) 基準との差 +0.00%
税引後で再投資(1株単位・端数は現金) 基準との差 -14.27%
税引後で再投資(単元単位でしか買えない) 基準との差 -17.51%
この仮の入力では、税を無視するかどうかで14ポイント、買付単位を100株にするとさらに3ポイントの開きが出ました。値そのものは入力しだいで動きます。読み取ってほしいのは「どのモデルを選んだか」が入力値と同じくらい効くということです。
だからこのツールはどのモデルが現実に近いかを判定しません。買付単位も税率も引数で受け取り、3本並べるところまでを担当します。既製のシミュレーターを使うときも、税引前か税引後か・端数をどう扱ったかが書いてあるかを先に探すと、数字の意味が変わります。
8. matplotlibで描く — 図の中に前提を焼き込む
グラフは記事から切り離されて出回ります。前提が本文にしか書いていなければ、図は「20年後の資産推移」という顔をして独り歩きする。そこで入力した条件と免責を図の中に描き込みます。
fig.text(0.5, 0.015, assumption_text(args) + "\n"
"この図は入力した仮定を機械的に計算した結果です。将来の結果を予測・保証するものではありません。",
ha="center", va="bottom", fontsize=8,
bbox=dict(facecolor="white", alpha=0.9, edgecolor="#c9ced6",
boxstyle="round,pad=0.4"))
ax.text はデータ座標に置く注記用なので、図全体への注記は fig.text が素直です。bbox を渡すと文字の背後に枠が付き、線と重なっても読めます。subplots_adjust(bottom=0.22) で下側を空けないと、注記が軸ラベルと重なります。
目盛りを「万円」にする関数は2引数
金額の軸をそのまま描くと 1e7 のような指数表記になります。FuncFormatter で万円表記に変えられますが、渡す関数は必ず2引数です。
def man_yen_formatter():
"""目盛りを「万円」表記にする。関数は (値, 位置) の2引数が必須。"""
from matplotlib.ticker import FuncFormatter
return FuncFormatter(lambda x, pos: f"{x / 10000:,.0f}")
うっかり lambda x: ... と書くと、描画のタイミングで落ちます。
TypeError: <lambda>() takes 1 positional argument but 2 were given
目盛りの文字を変えてもデータの単位は変わりません。set_ylabel("株式評価額+現金(万円)") と軸ラベルもそろえないと、桁が10000分の1の値に円の単位が付いた図になります。
対数軸は、0を黙って落とす
複利の推移は対数軸で描くと直線に近づくので、対数軸を勧める記事は多くあります。ただし0を含む系列を渡すと、例外も警告も出ないまま点が消えます。
ax.plot([0, 1, 2, 3], [0.0, 1000000.0, 1100000.0, 1210000.0], marker="o")
ax.set_yscale("log")
fig.canvas.draw()
print("例外なし。ylim =", ax.get_ylim())
# 例外なし。ylim = (np.float64(990514.2582145233), np.float64(1221587.6651600343))
0円の点は表示範囲の外に置かれ、グラフからはただ消えたように見えます。matplotlibの LogScale は「Care is taken to only plot positive values.」と設計を明示していて、0以下の値は既定でクリップされます。元本0から積み立てる系列を対数軸で描くと、最初の点が黙って落ちる。本ツールは線形軸を既定にしています。
日本語のラベルは rc_context({"font.family": "Meiryo"}) で通ります(findfont は C:\Windows\Fonts\meiryo.ttc を返し、代替フォントに落ちません)。第11回のtkinterでは同じ「Meiryo」の指定が効かず「Meiryo UI」を使いましたが、フォントの解決はライブラリごとに別の仕組みです。片方の結論をもう片方に持ち込まないでください(第7回・第5回も参照)。
色にも注意しています。2本の線に「良い色」と「悪い色」を割り当てない。再投資した系列を緑、しなかった系列を赤にした瞬間、図は計算結果ではなく主張になります。
9. 動かす — CLI・CSV・株価が下がる仮定・第8回DBの読み取り
ここまでの部品をつないだのが reinvest_sim.py です。7つの引数を渡すと年次の表が出ます。
$ python reinvest_sim.py --shares 1000 --price 2000 --dps 60 \
--dividend-growth 5 --price-growth 2 --tax-rate 20.315 --years 20 \
--csv result.csv --png assets.png
前提(すべて入力値・動作確認用の仮の値): 初期 1,000株 / 株価 2,000円 / 1株配当 60.00円 / 増配率 5.0% / 株価上昇率 2.0% / 税率 20.315% / 20年 / 1株単位
--------------------------------------------------------------------------
年 株価(円) 受取配当(円) 買増(株) 保有(株) 現金(円) 評価額(円)
0 2,000 0 0 1,000 0 2,000,000
1 2,040 47,811 23 1,023 891 2,087,811
(中略)
20 2,972 214,811 73 1,851 590 5,501,567
--------------------------------------------------------------------------
再投資しなかった場合との差: +20.84%(同じ仮定どうしの比較で、どちらが良いかを示すものではありません)
CSVは utf-8-sig で書き出し、1行目のコメントに前提条件を残します。表計算ソフトで開いたときに、どの仮定で作った表なのかが分からなくならないようにするためです。
with path.open("w", encoding="utf-8-sig", newline="") as f:
f.write(f"# {header_note}\n")
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
CSVと図の中身は「自分の資産についてどんな仮定を置いたか」の記録です。保有株数と株価が入っているので、共有フォルダ・クラウド同期フォルダ・Webサーバーの公開ディレクトリには置かないでください。渡す必要があるときは、1行目の前提コメントを外すかどうかを先に決めます。
株価が下がる仮定を入れる
株価上昇率と増配率にはマイナスも渡せます。下がるケースを1つも見せないツールは、上がる前提の道具になってしまうので、ここは必ず試してください。
$ python reinvest_sim.py ... --dividend-growth -5 --price-growth -4
(中略)
20 884 27,640 31 1,563 254 1,381,954
再投資しなかった場合との差: -7.71%(同じ仮定どうしの比較で、どちらが良いかを示すものではありません)
この仮定では再投資した側のほうが名目額で小さくなりました。3章で書いたとおり、再投資はその瞬間に資産を増やす操作ではなく、現金を株に置き換える操作です。置き換えた先の価格が下がり続ければ、現金のまま持っていた場合を下回ります。「再投資すれば増える」ではなく、入力した仮定が結果の向きを決めていることが、この1本の図で分かります。
第8回のDBを読み取り専用で開く
--from-db を付けると、第8回で作った dividends.db から直近1年の入金合計だけを読んで表示します。計算には使いません。
uri = f"file:{db_path.as_posix()}?mode=ro"
try:
with sqlite3.connect(uri, uri=True) as conn:
row = conn.execute(
"SELECT COALESCE(SUM(net_sen), 0) FROM dividends "
"WHERE paid_on >= date('now', '-1 year')").fetchone()
読めなければ sqlite3.OperationalError を受けて None を返し、読めた値は銭から円に直します(第8回は金額を銭単位の整数で持っています)。mode=ro は読み取り専用で開くURIモードで、書き込もうとすると例外になります。
>>> con = sqlite3.connect(f"file:{p.as_posix()}?mode=ro", uri=True)
>>> con.execute("INSERT INTO stocks VALUES ('X','Y','Z')")
sqlite3.OperationalError: attempt to write a readonly database
ファイルが存在しないときも新しいDBを作らずに例外になります(通常の connect() は空のDBを作ります)。第9回で「dividends.db には1行も書き込まない」と決めた約束を、本記事は接続モードで守っています。この機能は入力値を考えるときの参考表示で、DBが無くてもツールは動きます。
10. 配当フェーズ5本の回収と、このツールで分からないこと
4〜8章で踏んだ地雷には共通点があります。名前と引数の数を間違えたとき以外、どれも例外を出しません。丸めも、float32 の刻みも、対数軸から消えた0も、結果の数字だけを静かに変えていきます。
この記事のつまずきはライブラリが黙って決めた仕様に集まりました。np.round は偶数丸め、isclose は相対誤差、対数軸は0をクリップ、cumprod は指定したdtypeのまま桁あふれ——どれもエラーを出さずに結果だけ変えます。「エラーが出ないまま結果だけ変わる」を書く前に潰すのが、Effective Python 第3版の125項目です(当サイトのおすすめ本ランキング総合4位・全572ページ)。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。
配当フェーズ5本が答えたこと・答えられないこと
| 回 | 作ったもの | 答えられること | 答えられないこと |
|---|---|---|---|
| 第8回 | 配当金管理ツール | いつ・いくら入金されたか(実績) | 記録していない口座の入金 |
| 第9回 | 配当金カレンダー | 1年の入金がどの月に寄っているか | 今年の配当が去年と同じか |
| 第10回 | 配当利回りスクリーナー | 入力したデータのどの行が条件に合うか | その条件が妥当かどうか |
| 第11回 | 株主優待カレンダー | 権利確定月と必要株数の管理 | 優待の価値 |
| 第12回 | 本記事の再投資シミュレーター | 入力した仮定なら計算上どうなるか | その仮定が起きるかどうか |
第8回は起きたことを、第9回・第10回は手元のデータの中身を答えられました。本記事が答えられるのは入力した仮定の中だけです。仮定の外側——実際の受取額、減配や無配、株価の動き、売買コスト、制度の改正、そして自分に合うやり方かどうか——について、このツールは何も言いません。
既製のシミュレーターと自作の使い分け
| 見る観点 | Webのシミュレーター | 本記事の自作 |
|---|---|---|
| 使い始めるまで | 入力欄を埋めるだけ | Pythonの実行環境が要る |
| 前提の決め方 | 用意された項目の範囲 | 税・買付単位・年数を自分で決める |
| 計算の中身 | 結果だけが表示される | 式を読める・変えられる |
| 税と端数 | 税引前/税引後や小数株の扱いはツールによる | どちらも引数で明示する |
ざっと当たりを付けるだけなら入力欄を埋めるほうが速いです。自作が受け持てるのは「その数字がどんな前提から出てきたのか」を自分で説明できるところだけ。金融庁のつみたてシミュレーターも「本シミュレーションのいかなる内容も、将来の結果を予測し、保証するものではありません」と明記しています(金融庁・2026年9月5日確認)。試算は前提の言い換えであって、将来の情報ではありません。
次回:Streamlitで投資ダッシュボード
第13回からは応用フェーズに入ります。Streamlitでブラウザ上で動く投資ダッシュボードを作る予定です。連載の全体像は投資×Python シリーズ一覧にあります。
本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事とコードに登場する株数・株価・配当額・利回り・増配率・株価上昇率・税率・年数はすべて動作確認用の仮の値で、推奨値でも予測でもありません。金融商品取引法38条2号は不確実な事項について断定的判断を提供することを禁じており、金融庁の監督指針も「利回りの保証若しくは損失の全部若しくは一部の負担を行う旨の表示又はこれを行っていると誤解させるような表示」を戒めています(金融商品取引業者等向けの総合的な監督指針)。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、個別銘柄の推奨・売買タイミングの助言・税務相談には応じられません。税制の適用や確定申告の要否は国税庁の情報か税理士にご確認ください。投資判断はご自身の責任で(詳細は免責事項)。
11. よくある質問(FAQ)
Q. このツールが出す数字は当たりますか?
当たりません。これは予測する道具ではなく、入力した仮定を機械的に計算する道具です。利回りも増配率も株価上昇率も、利用者が「もしこうだったら」と決めた値で、そうなる根拠はどこにもありません。金融庁のつみたてシミュレーターも「将来の結果を予測し、保証するものではありません」と明記しています(10章)。
Q. np.fv が無いと言われます。どうすればいいですか?
NumPy 1.18で非推奨、1.20で削除済みです(NEP 32)。検索で出てくる古いコードはそのままでは動きません。本記事のように np.cumprod で式を自分で書くのが確実です(4章)。numpy-financial を入れれば npf.fv() は使えますが、あれは定額積立の元利計算で、増配率・税率・買付単位を表現できません。np.cumproduct・np.round_ などもNumPy 2.0で削除されています。
Q. なぜ利回りや税率に既定値が無いのですか?
既定値を置くと、その数字が推奨値として読まれるからです。「とりあえず実行したら5%で計算された」という体験は、5%という見立てを配ったのと同じことになります。7つの前提はすべて必須引数にしてあり、何を入れるかは利用者が決めます(2章)。
Q. 税率には何を入れればいいですか?
本記事では断定しません。2026年9月時点の一般的な源泉徴収は国税庁 No.1330・No.1331 で確認できますが、2027年1月から内訳が変わることが公表されており(防衛特別所得税の創設・復興特別所得税の引き下げ)、口座の種類や制度によっても変わります(3章)。最新の国税庁の情報を確認するか、税理士にご相談ください。非課税で計算したい場合は --tax-rate 0 を渡せます。
Q. --from-db を使うと第8回のDBは書き換わりますか?
書き換わりません。file:...?mode=ro の読み取り専用モードで開いており、書き込もうとすると sqlite3.OperationalError: attempt to write a readonly database になります。読むのは直近1年の入金合計だけで、計算にも使いません(9章)。DBが無くてもツールは動きます。なお、このツールは外部と一切通信しません。
Q. 結局、配当は再投資したほうが得なのですか?
本記事では判断しません。同じ仮定で計算すると再投資した側の名目額が大きくなる入力もあれば、株価と配当が下がる仮定では再投資した側が小さくなる入力もあります(9章)。受け取った配当を再投資に回すか使うかは目的しだいです。ツールにできるのは、置いた仮定を上下に振って答えの動く幅を見せるところまで。まず下の3本から手を動かしてみてください。
まずは完成版 reinvest_sim.py を保存して、7つの引数を自分で決めて動かすところまで。そのあと --price-growth にマイナスを入れて、図がどう変わるかを見てください。
入金の記録は第8回、numpyやmatplotlibの位置づけはよく使うPythonライブラリ50選にあります。連載の全体像は投資×Python シリーズ一覧(ブックマーク推奨)か新着記事のRSSで追えます。
本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・画像は Windows 11 / Python 3.12.10 / numpy 2.4.6 / matplotlib 3.11.1 で 2026年9月5日に実行して確認したものです。記事とコードに登場する株数・株価・配当額・利回り・増配率・株価上昇率・税率・年数はすべて動作確認用の仮の値であり、推奨値でも予測でもありません。本ツールの出力は、入力した仮定を機械的に計算した結果であって、将来の運用成果を予測・保証するものではありません。本記事はプログラミングの情報提供を目的としたもので、銘柄の売買・投資手法の推奨でも税務上の助言でもありません。投資判断はご自身の責任で。AIの利用方針は免責事項をご覧ください。