Skip to the content.

← 目次← 前: 04次: 06 →

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(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 棄却 

← 目次← 前: 04次: 06 →