Skip to the content.

← 目次← 前: 03次: 05 →

04. 表形式の出力と累積計算

テーマ: 住宅ローンの元利均等返済スケジュール

学習点: ジェネレータ関数(yield), 名前付きタプル, 集計, 表の整形

依存: 標準ライブラリのみ / 難易度: 基礎

実行方法

uv run 04_loan_amortization.py

スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは uv が自動的に仮想環境へ導入します。事前の pip install は不要です。

解説

何をするプログラムか

住宅ローンで最も一般的な「元利均等返済」は、毎月の返済額を一定にする方式です。返済額のうち利息と元本の内訳は毎月変わり、当初は利息の割合が大きく、返済が進むにつれて元本の割合が増えていきます。このスクリプトは、借入 3,500 万円・年利 1.5%・35 年(420 回)という現実的な条件で毎月の返済額を計算し、各回の「返済額・うち利息・うち元本・残高」を並べた返済予定表(償還表)を作ります。

さらに総返済額と総利息を集計し、年利を 0.5%〜3.0% に振ったときに総利息がどれだけ変わるかという金利感応度の比較も行います。

コードの読みどころ

理論的背景

毎月返済額の公式 A = P·i / (1 − (1+i)^(−n))(i は月利、n は回数)は、年金現価の関係式から導かれます。毎月 A 円を n 回受け取る年金の現在価値は P = A·(1 − (1+i)^(−n))/i(年金現価係数 × A)であり、「借入額 = 将来の全返済額の現在価値」と置いてこの式を A について解いたものが上の公式です。つまり毎月返済額は、借入額を年金現価係数で割った値になっています。各回の利息は直前残高 × 月利で決まるため、残高が減るほど利息が減り、その分だけ元本返済が加速します。

実行結果の見方

毎月返済額は 107,165 円で 420 回一定です。第 1 回は利息 43,750 円・元本 63,415 円ですが、第 420 回では利息 134 円・元本 107,031 円と内訳が逆転し、残高はちょうど 0 になります。総返済額は約 4,501 万円で、利息だけで 1,001 万円、元本の 28.6% に相当します。

金利感応度の表では、年利 0.5% なら総利息 316 万円、3.0% なら 2,157 万円と、金利 6 倍で総利息は約 6.8 倍になります。35 年という長期の借入では、わずか 0.5% の金利差が数百万円の差になることが読み取れます。

ソースコード

# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""04: 表形式の出力と累積計算 ---------------------------------------------
テーマ: 住宅ローンの元利均等返済スケジュール
学習点: ジェネレータ関数(yield), 名前付きタプル, 集計, 表の整形
根拠: 元利均等の毎月返済額 A = P * i / (1 - (1+i)^-n)  (i=月利, n=回数)
      これは年金現価係数の逆数で、P = A * (1-(1+i)^-n)/i を A について解いたもの。
"""
from typing import Iterator, NamedTuple


class Row(NamedTuple):
    month: int
    payment: float
    interest: float
    principal: float
    balance: float


def monthly_payment(principal: float, annual_rate: float, years: int) -> float:
    i = annual_rate / 12
    n = years * 12
    if i == 0:
        return principal / n
    return principal * i / (1 - (1 + i) ** (-n))


def schedule(principal: float, annual_rate: float, years: int) -> Iterator[Row]:
    """毎月の返済明細を1行ずつ生成する(ジェネレータ=遅延評価)。"""
    i = annual_rate / 12
    a = monthly_payment(principal, annual_rate, years)
    bal = principal
    for m in range(1, years * 12 + 1):
        interest = bal * i
        prin = a - interest
        bal = max(bal - prin, 0.0)
        yield Row(m, a, interest, prin, bal)


def main() -> None:
    P, r, Y = 35_000_000, 0.015, 35
    a = monthly_payment(P, r, Y)
    print(f"借入 {P:,}円 / 年利 {r:.2%} / {Y}年({Y*12}回)")
    print(f"毎月返済額: {a:,.0f}\n")

    rows = list(schedule(P, r, Y))
    print(f"{'回':>4}{'返済額':>12}{'うち利息':>12}{'うち元本':>12}{'残高':>15}")
    for row in rows[:3] + rows[-2:]:
        print(f"{row.month:>4}{row.payment:>12,.0f}{row.interest:>12,.0f}"
              f"{row.principal:>12,.0f}{row.balance:>15,.0f}")

    total_i = sum(x.interest for x in rows)
    print(f"\n総返済額 {sum(x.payment for x in rows):,.0f}円 "
          f"(うち利息 {total_i:,.0f}円 = 元本の {total_i / P:.1%})")

    print("\n[金利感応度] 総利息の比較")
    for rr in [0.005, 0.010, 0.015, 0.020, 0.030]:
        ti = sum(x.interest for x in schedule(P, rr, Y))
        print(f"  年利 {rr:.2%}: 月々 {monthly_payment(P, rr, Y):>9,.0f}円 / "
              f"総利息 {ti:>13,.0f}円")


if __name__ == "__main__":
    main()

実行結果

借入 35,000,000円 / 年利 1.50% / 35年(420回)
毎月返済額: 107,165 円

   回         返済額        うち利息        うち元本             残高
   1     107,165      43,750      63,415     34,936,585
   2     107,165      43,671      63,494     34,873,092
   3     107,165      43,591      63,573     34,809,518
 419     107,165         267     106,897        107,031
 420     107,165         134     107,031              0

総返済額 45,009,113円 (うち利息 10,009,113円 = 元本の 28.6%)

[金利感応度] 総利息の比較
  年利 0.50%: 月々    90,855円 / 総利息     3,159,050円
  年利 1.00%: 月々    98,800円 / 総利息     6,495,998円
  年利 1.50%: 月々   107,165円 / 総利息    10,009,113円
  年利 2.00%: 月々   115,942円 / 総利息    13,695,627円
  年利 3.00%: 月々   134,698円 / 総利息    21,572,978円

← 目次← 前: 03次: 05 →