05. 数値解法
テーマ: 投資評価(NPV・IRR・回収期間)
学習点: 高階関数, 二分法とニュートン法, 例外処理, 収束判定
依存: 標準ライブラリのみ / 難易度: 基礎
実行方法
uv run 05_npv_irr.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
設備投資や新製品開発の採否を判断する代表的な指標が、正味現在価値(NPV)、内部収益率(IRR)、回収期間の 3 つです。NPV は将来のキャッシュフローを割引率で現在価値に換算して合計したもの、IRR は「NPV がちょうどゼロになる割引率」、回収期間は初期投資を回収するまでの年数です。
このスクリプトは、初期投資とその後 5 年のキャッシュフローが異なる 3 案件(設備更新・新製品・短期案件)について 3 指標を一覧表にし、IRR を求める 2 つの数値解法(二分法とニュートン法)の結果を突き合わせ、最後に割引率を 0%〜25% と動かして NPV がどう減っていくかをテキストの棒グラフで可視化します。IRR には閉じた公式がないため、方程式を数値的に解く技法を学ぶ格好の題材です。
コードの読みどころ
npv(rate, cashflows)はsum(cf / (1 + rate) ** t for t, cf in enumerate(cashflows))の 1 行で、定義式 Σ CF_t/(1+r)^t をenumerateの添字 t とともに素直に書いています。irr_bisection()は二分法です。区間 [lo, hi] の中点で NPV の符号を調べ、解のある側へ区間を半分に狭める操作を最大 200 回繰り返し、abs(f_mid) < tolを収束判定とします。冒頭のif f_lo * f_hi > 0: raise ValueError(...)は前提(区間端で符号が異なる)が崩れたときに例外を送出する防御的な書き方です。irr_newton()はニュートン法で、NPV の導関数(d = sum(-t * c / (1 + r) ** (t + 1) ...))を使ってr - f / dと更新します。収束は速い一方、初期値guess=0.1に依存する点がコメントで注意されています。main()のループではtry: ... except ValueError as e:が IRR 計算不可の案件を握りつぶさずに報告し、continueで次の案件へ進みます。payback_period()は累積キャッシュフローcumが負から非負へ変わる年を検出し、t - 1 + (-prev) / cの線形補間で小数年を返します。解がなければNone(型ヒントはfloat | None)です。
理論的背景
NPV(r) = Σ CF_t/(1+r)^t は「将来の 1 円は今の 1 円より価値が低い」という時間価値の考えを式にしたもので、NPV > 0 の案件は資本コスト r を上回る価値を生むため採択、という判断基準になります。IRR はこの NPV(r) = 0 の解です。
初期投資が負で以後のキャッシュフローが正という「通常型」では、係数列の符号転換が 1 回しかないため、デカルトの符号法則により正の解は高々 1 つに定まります。しかも NPV(r) は r の単調減少関数なので、符号の異なる区間から出発する二分法は必ず一意の解に収束します。符号転換が複数ある変則的なキャッシュフローでは IRR が複数存在し得ることも、この法則から分かります。
実行結果の見方
一覧表では、NPV(5% と 10% の 2 通り)・IRR・回収期間が案件ごとに並びます。NPV では B(新製品)が最大ですが、回収の速さでは C(短期案件、1.60 年)が優れ、指標によって案件の順位が変わり得ることが分かります。[解法の比較] では二分法とニュートン法がともに 0.2032984926 と 10 桁一致し、独立な 2 解法の一致が検算になっています。
最後の棒グラフでは、割引率が上がるほど NPV が単調に減り、20% では +15.0 でぎりぎり採択、IRR(20.33%)を超えた 25% で −191.7 の棄却に転じます。「IRR は採択と棄却の境目の割引率」という定義が視覚的に確認できます。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""05: 数値解法 -----------------------------------------------------------
テーマ: 投資評価(NPV・IRR・回収期間)
学習点: 高階関数, 二分法とニュートン法, 例外処理, 収束判定
根拠: NPV(r) = Σ CF_t/(1+r)^t。IRR は NPV(r)=0 となる r。
通常のキャッシュフロー(初期投資が負、以後正)では NPV は r の単調減少関数
となり解が一意なので二分法が確実に収束する(デカルトの符号法則)。
"""
from typing import Sequence
def npv(rate: float, cashflows: Sequence[float]) -> float:
return sum(cf / (1 + rate) ** t for t, cf in enumerate(cashflows))
def irr_bisection(cf: Sequence[float], lo=-0.99, hi=10.0, tol=1e-10) -> float:
"""二分法。区間端で符号が異なることを前提とする。"""
f_lo, f_hi = npv(lo, cf), npv(hi, cf)
if f_lo * f_hi > 0:
raise ValueError("指定区間に解がありません(符号が同じ)")
for _ in range(200):
mid = (lo + hi) / 2
f_mid = npv(mid, cf)
if abs(f_mid) < tol:
return mid
if f_lo * f_mid < 0:
hi, f_hi = mid, f_mid
else:
lo, f_lo = mid, f_mid
return (lo + hi) / 2
def irr_newton(cf: Sequence[float], guess=0.1, tol=1e-12) -> float:
"""ニュートン法。収束は速いが初期値に依存する。"""
r = guess
for _ in range(100):
f = npv(r, cf)
d = sum(-t * c / (1 + r) ** (t + 1) for t, c in enumerate(cf))
if abs(d) < 1e-15:
break
r_new = r - f / d
if abs(r_new - r) < tol:
return r_new
r = r_new
return r
def payback_period(cf: Sequence[float]) -> float | None:
"""単純回収期間(線形補間)。"""
cum = 0.0
for t, c in enumerate(cf):
prev = cum
cum += c
if prev < 0 <= cum and t > 0:
return t - 1 + (-prev) / c
return None
def main() -> None:
projects = {
"A: 設備更新": [-1000, 300, 350, 400, 420, 300],
"B: 新製品": [-1500, 100, 300, 600, 900, 1100],
"C: 短期案件": [-500, 320, 300, 60, 0, 0],
}
print(f"{'案件':<12}{'NPV(5%)':>11}{'NPV(10%)':>11}{'IRR':>9}{'回収期間':>10}")
print("-" * 55)
for name, cf in projects.items():
try:
r = irr_bisection(cf)
except ValueError as e:
print(f"{name:<12} IRR計算不可: {e}")
continue
pb = payback_period(cf)
print(f"{name:<12}{npv(0.05, cf):>11,.1f}{npv(0.10, cf):>11,.1f}"
f"{r:>9.2%}{pb:>9.2f}年")
cf = projects["B: 新製品"]
print(f"\n[解法の比較] 案件B 二分法={irr_bisection(cf):.10f} "
f"ニュートン法={irr_newton(cf):.10f}")
print("\n[割引率とNPVの関係] 案件B — 割引率がIRRを超えると採択不可になる")
for r in [0.00, 0.05, 0.10, 0.15, 0.20, 0.25]:
v = npv(r, cf)
bar = "#" * max(int(v / 40), 0)
print(f" r={r:>5.0%} NPV={v:>9,.1f} {'採択' if v > 0 else '棄却'} {bar}")
if __name__ == "__main__":
main()
実行結果
案件 NPV(5%) NPV(10%) IRR 回収期間
-------------------------------------------------------
A: 設備更新 529.3 335.7 22.09% 2.88年
B: 新製品 988.0 587.4 20.33% 3.56年
C: 短期案件 128.7 83.9 21.51% 1.60年
[解法の比較] 案件B 二分法=0.2032984926 ニュートン法=0.2032984926
[割引率とNPVの関係] 案件B — 割引率がIRRを超えると採択不可になる
r= 0% NPV= 1,500.0 採択 #####################################
r= 5% NPV= 988.0 採択 ########################
r= 10% NPV= 587.4 採択 ##############
r= 15% NPV= 269.8 採択 ######
r= 20% NPV= 15.0 採択
r= 25% NPV= -191.7 棄却