還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

華為雲開發者聯盟發表於2023-02-22
摘要:透過樓市小程式上貸款計算器等工具人們可以很容易的瞭解每期還款本金、不同還款方式的利息差異、提前還款節省利息等問題。

本文分享自華為雲社群《房貸計算器-從原理、計算到提前還款和視覺化》,作者: 蜉蝣與海 。

前言

最近各地樓市震盪不斷,2022年12月份以來不少銀行紛紛降息,隨後更是引發了一波提前還款的大潮。不少地區樓市相關的微信小程式也自帶了貸款計算器、提前還款計算器等工具,透過這些工具人們可以很容易的瞭解每期還款本金、等額本金/本息的利息差異、提前還款節省利息的問題。

瞭解這些計算工具的相關原理,可以做到心中有數,臨危不慌。

注:

  1. 本文對應程式碼和指令碼釋出至華為雲生態社群AI Gallery:貸款計算器-從原理、公式到提前還款和視覺化歡迎開發者前往體驗,文中涉及所有程式碼可以直接透過頁面進入ModelArts Code Lab執行。使用該指令碼稍加修改後即可嘗試開發一個適合自身地區政策的貸款計算&提前還款小程式。
  2. 本文只是研究貸款生成、提前還貸方面的相關計算原理,不構成任何投資理財方面的建議。

如何計算利息

背景:等額本金和等額本息的共同點

瞭解過貸款的小夥伴都知道,貸款有等額本金和等額本息這兩種方式,前者每月還款的本金相同,利息逐月遞減;後者每月還款額相同,剛開始還款時利息還的多,後面本金還的逐漸增多。參考網上討論利息計算的諸多文章,兩個模型理論上,都有下列共同特點:

利息按月利率計算,一月一期

按期還款情況下當月應還利息只由未還完的本金決定

每月還款額除了未還本金產生的全部利息外,剩下的金額應該全部用於償還本金

像最近部分銀行提出的先息後本(先還利息若干年,最後一次性償還本金)則不符合這個條件。

還款額的計算

前陣子,院長有位朋友在惠州買了套120平米的房,總價125萬左右,大約貸了87.5萬。 辦房貸的時候,他聽從銷售的建議,選了等額本息的還款方式。每個月固定還5726.39元。這個還款額度在他的承受範圍之內,因此就選了。 那假如選擇等額本金呢?第一個月要還的金額為7218.75元,此後每個月少還14.89元,直至20年後還完。

知乎文章為什麼買房貸款,最好選擇等額本金?中提到了一個例子:

透過描述可知,貸款87.5萬,貸20年,等額本息每月還款5726.39元,等額本金首月還款7218.75元。假設文中的貸款未使用公積金,計算時利率為固定利率,根據網上的貸款計算器可知此時的貸款年利率為4.9%。

以這個例子為例,簡單說明等額本金和等額本息的計算方法:

首先貸20年,按月分期,貸款為 20 × 12 = 240期。 年利率4.9%,月利率為 0.049 ÷ 12 = 0.004983 即0.4083%。

等額本金 情況下:

  • 每月應還本金=總本金 ÷ 期數
  • 每月應還利息=剩餘本金×月利率
  • 每月還款額=每月應還本金 + 每月應還利息

在這個例子中:

  • 每月應還本金為875000÷240=3645.83元
  • 首月應還利息為875000×0.4083%=3572.92元
  • 首月應還:3645.83 + 3572.92 = 7218.75元。
  • 第2月剩餘本金為875000 - 3645.83 = 871354.17元。
  • 第2月應還利息為871354.17×0.4083%=3558.03元。
  • 第2月應還:3645.83 + 3558.03 = 7203.86元。

將這段邏輯抽象為程式碼有:

import matplotlib.pyplot as plt
import numpy as np
def averageCapital(months, principal, rate):
 month_rate = rate / 12
 monthly_capital = principal / months
    interests = [0] * months
    capitals = [0] * months
 left_principal = [0] * months
 left_principal[0] = principal
 total_payment = [0] * months
 for i in range(0, months):
        interests[i] = left_principal[i] * month_rate
        capitals[i] = monthly_capital
 total_payment[i] = monthly_capital + interests[i]
 if i + 1 < months:
 left_principal[i + 1] = left_principal[i] - monthly_capital
 return capitals, interests, total_payment

為了便於檢視再封裝一個列印成表格的函式:

import pandas as pd
def drawTable(months, fn, *args, **kwargs):
    capitals, interests, total_payment = fn(months, *args, **kwargs)
 paid_capital = [0] * months
 paid_interests = [0] * months
 paid_capital[0] = capitals[0]
 paid_interests[0] = interests[0]
 for x in range(1, months):
 paid_capital[x] = paid_capital[x - 1] + capitals[x]
 paid_interests[x] = paid_interests[x - 1] + interests[x]
    origin = pd.DataFrame([total_payment, capitals, interests, paid_capital, paid_interests])
 return pd.DataFrame(origin.values.T, columns=['還款額','還款本金','還款利息','已還本金','已還利息'], index=np.arange(1, months + 1))

我們執行一下知乎上的例子,看看頭幾年還款的本金、利息等:

pd.options.display.float_format = '{:.2f}'.format
drawTable(12 * 20, averageCapital, 875000, 0.049)[0:10]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

可以看到和文中描述一致,使用微信房小團小程式,也可以列印出一致的結果。

等額本息 的計算方法有些複雜,參考用Python深度解讀房貸利率文中的解法,設A為本金,第i個月月末所欠銀行本金為Ai,每月所還貸款總額為X,月利率為β, 則有:

還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

由於最後一期時剩餘本金為0,可反解得:

還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

這裡m為總期數(在剛剛的例子中,m=240)。而後就可以使用與等額本金計算中類似的邏輯,從第一期所還利息開始,反推每期的利息與本金。具體程式碼如下:

def averageCapitalPlusInterest(months, principal, rate):
 month_rate = rate / 12
 monthly_payment = principal * month_rate * (1 + month_rate) ** months / ((1 + month_rate) ** months - 1)
    interests = [0] * months
    capitals = [0] * months
 left_principal = [0] * months
 left_principal[0] = principal
 total_payment = [0] * months
 for i in range(0, months):
 total_payment[i] = monthly_payment
        interests[i] = left_principal[i] * month_rate
        capitals[i] = total_payment[i] - interests[i]
 if i + 1 < months:
 left_principal[i + 1] = left_principal[i] - capitals[i]
 return capitals, interests, total_payment

我們執行一下知乎上的例子,看看等額本息模式下第8年附近,到底還了多少利息和本金:

drawTable(12 * 20, averageCapitalPlusInterest, 875000, 0.049)[90:100]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

可以看到第96期(第8年年終)時,本金還了25萬,但利息已經還了近30萬了,和之前文中例子的資料是可以對得上的。

還款視覺化

剛剛我們已經將還款的各項資料以表格的形式列印。此外我們還可以藉助python的能力,列印還款的柱狀圖。

import numpy as np
def printStatistics(capitals, interests, total_payment, months):
 print("總本金:" + str(np.sum(capitals)))
 print("總利息:" + str(np.sum(interests)))
 print("總利息/總本金" + str(np.sum(interests)/np.sum(capitals)))
 print("首月還款 %.2f 末月還款: %.2f" % (total_payment[0], total_payment[months - 1]))
def drawDiagram(months, fn, *args, **kwargs):
    capitals, interests, total_payment = fn(months, *args, **kwargs)
 printStatistics(capitals, interests, total_payment, months)
 month_array = np.arange(1, months + 1, 1)
    height = interests
 plt.bar(month_array, capitals, width=0.2, align='center', color='red')
 plt.bar(month_array, interests, width=0.2, align='center', color='blue', bottom=capitals)
 plt.show()

再跑一下知乎的例子,繪製等額本金和等額本息的還款柱狀圖:

drawDiagram(12 * 20, averageCapital, 875000, 0.049)
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

如圖,藍色是所還利息,紅色是所還本金。可以看出本金每月不變,利息逐月遞減的特徵。

等額本息情況下:

drawDiagram(12 * 20, averageCapitalPlusInterest, 875000, 0.049)
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

也能看出所繪圖形和等額本息的含義基本一致。

另外部分城市可以公積金貸款,以杭州為例,目前杭州公積金充足情況下可貸50w-60w,這裡考慮一下公積金的情況:

def averageCapitalWithPublicFund(months, principal1, rate1, principal2, rate2):
    a, b, c = averageCapital(months, principal1, rate1)
    a1, b1, c1 = averageCapital(months, principal2, rate2)
 return np.sum([a,a1],axis=0).tolist(), np.sum([b,b1],axis=0).tolist(), np.sum([c,c1],axis=0).tolist()
drawTable(12 * 20, averageCapitalWithPublicFund, 700000, 0.041, 300000, 0.031)[0:10]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

這裡算了下商貸70w(利率4.1%),公積金貸30w(利率3.1%)下組合貸款的情況,和微信小程式房小團的計算是一致的。

提前還款相關原理

再來討論下提前還款。如果知乎文中買房的那位,在貸款1年後提前還款10w會怎樣呢?瞭解一點背景知識的朋友,都知曉提前還款分兩種情況:

  • 年限不變,月供減少
  • 年限縮短,月供不變

現在分情況討論,並給出計算函式。

注:notebook中所有計算結果均在微信房小團小程式上得到互相驗證。

年限不變,月供減少

這種情況下,相當於在提前還款月之後重新做了一次貸款。我們首先對剛剛的計算函式進行一定的簡化,抽象一下公共的部分。

def normalPaid(months, principal, rate, capitalAveraged):
 month_rate = rate / 12
 monthly_capital = principal / months
 monthly_payment = principal * month_rate * (1 + month_rate) ** months / ((1 + month_rate) ** months - 1)
    interests = [0] * months
    capitals = [0] * months
 left_principal = [0] * months
 left_principal[0] = principal
 total_payment = [0] * months
 for i in range(0, months):
        interests[i] = left_principal[i] * month_rate
 if capitalAveraged:
            capitals[i] = monthly_capital
 total_payment[i] = monthly_capital + interests[i]
 else:
 total_payment[i] = monthly_payment
            capitals[i] = total_payment[i] - interests[i]
 if i + 1 < months:
 left_principal[i + 1] = left_principal[i] - capitals[i]
 return capitals, interests, total_payment

 

drawTable(12 * 20, normalPaid, 875000, 0.049, False)[10:14]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然
drawTable(12 * 20, normalPaid, 875000, 0.049, True)[10:14]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

可以看到抽象出公共結構後,前後的計算結果並沒有發生變化。

考慮年限不變提前還款的情況,這裡將每次提前還款的時間和金額組成python的元組,若干個(賬期,還款金額)元組組成一個list輸入函式。函式首先計算正常情況下的還款資訊,而後根據提前還款資訊,修改提前還款日的剩餘本金,並從各個提前還款日重新計算剩餘還款。

def extraPaidWithFixedPeriod(months, principal, rate, capitalAveraged, extraPaidList :list):
    capitals, interests, total_payment = normalPaid(months, principal, rate, capitalAveraged)
 extraPaidList.sort(key=lambda x:x[0])
 originCapital, originInterests, originTotal = capitals.copy(), interests.copy(), total_payment.copy()
 left_principal = [0] * months
 left_principal[0] = principal
 for x in range(0,months):
 if x < months - 1:
 left_principal[x + 1] = left_principal[x] - capitals[x]
    def normalPaidOffset(left_months, principal, rate, capitalAveraged, offset):
 month_rate = rate / 12
 monthly_capital = left_principal[offset] / left_months
 monthly_payment = left_principal[offset] * month_rate * (1 + month_rate) ** left_months / ((1 + month_rate) ** left_months - 1)
 for i in range(0, left_months):
 interests[offset + i] = left_principal[offset + i] * month_rate
 if capitalAveraged:
 capitals[offset + i] = monthly_capital
 total_payment[offset + i] = monthly_capital + interests[offset + i]
 else:
 total_payment[offset + i] = monthly_payment
 capitals[offset + i] = total_payment[offset + i] - interests[offset + i]
 if i == 0:
 print("次月還款 %.2f" % total_payment[offset + i])
 if offset + i + 1 < months:
 left_principal[offset + i + 1] = left_principal[offset + i] - capitals[offset + i]
 return
 for x,y in extraPaidList:
        capitals[x] = capitals[x] + y
 left_principal[x + 1] = left_principal[x] - capitals[x]
 total_payment[x] = capitals[x] + interests[x]
 print("當月需還 %.f 剩餘本金 %.f" %(total_payment[x], left_principal[x + 1]))
 normalPaidOffset(months - x - 1, left_principal[x + 1], rate, capitalAveraged, x + 1)
 printStatistics(originCapital, originInterests, originTotal, months)
 print("")
 printStatistics(capitals, interests, total_payment, months)
 print("節省利息 %.2f" % (np.sum(originInterests) - np.sum(interests)))
 return capitals, interests, total_payment, originTotal, originInterests

再定義幾個函式對提前還款節省的利息進行視覺化。

def drawDiagramExtraPaid(months, capitals, interests, originalTotal, originalInterests, showOriginTotal=True):
 month_array = np.arange(1, months + 1, 1)
 capital_with_origin_interest = [0] * months
    height = interests
 for x in range(1, months):
 capital_with_origin_interest[x] = capitals[x] + originalInterests[x]
    l1 = plt.bar(month_array, originalTotal if showOriginTotal else capital_with_origin_interest, width=0.2, align='center', color='yellow')
    l2 = plt.bar(month_array, capitals, width=0.2, align='center', color='red')
    l3 = plt.bar(month_array, interests, width=0.2, align='center', color='blue', bottom=capitals)
    # plt.legend(handles = [l1, l2,l3], labels = ['每月少還' if showOriginTotal else '節省利息', '本金','利息'], loc = 'best',fontsize=20)
 plt.ylim(0, (capitals[0]+interests[0])*1.1)
 plt.show()
def drawTableExtraPaid(months, capitals, interests, total_payment, originalTotal, originalInterests):
 paid_capital = [0] * months
 paid_interests = [0] * months
 saved_money = [0] * months
 paid_capital[0] = capitals[0]
 paid_interests[0] = interests[0]
 for x in range(1, months):
 paid_capital[x] = paid_capital[x - 1] + capitals[x]
 paid_interests[x] = paid_interests[x - 1] + interests[x]
 saved_money[x] = saved_money[x - 1] + (originalInterests[x] - interests[x] )
    origin = pd.DataFrame([total_payment, capitals, interests, paid_capital, paid_interests,saved_money])
 return pd.DataFrame(origin.values.T, columns=['還款額','還款本金','還款利息','已還本金','已還利息','累計節省'], index=np.arange(1, months + 1))

透過引數showOriginTotal的取值,可以分別繪製每月少還的錢與當月節省利息的情況。下面分別繪製了等額本金和等額本息情況下,87.5萬貸20年,在第一年還10萬後還款和利息的變化情況。

a, b, c, d, e = extraPaidWithFixedPeriod(12 * 20, 875000, 0.049, True, [(13,100000)])
drawDiagramExtraPaid(12 * 20, a, b, d, e)
drawDiagramExtraPaid(12 * 20, a, b, d, e, False)
drawTableExtraPaid(12 * 20, a, b, c, d, e)[10:20]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然
a, b, c, d, e = extraPaidWithFixedPeriod(12 * 20, 875000, 0.049, False, [(13,100000)])
drawDiagramExtraPaid(12 * 20, a, b, d, e)
drawDiagramExtraPaid(12 * 20, a, b, d, e, False)
drawTableExtraPaid(12 * 20, a, b, c, d, e)[10:20]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

可以很方便地看出節省利息在每個月還款額中的比重。

月供不變,年限縮短

這種情況下提前還款導致後續每個月產生的利息少了,但是月供沒變,相當於後續每個月額外多還了本金。但是在各類提前還款計算器的計算中,月供並不是和之前相同的,經過反覆的計算後和網上的貸款計算器結果最終一致,發現各類提前還款計算器隱含了下列約束:

  • 提前還款相當於用剩餘本金新做一個貸款。
  • “月供”不是真的不變。而是透過縮短年限方式,使得新貸款首月月供儘可能和當前月供相當。
  • 如果是等額本金模式,新貸款首月月供中,償還本金並未增多,需要略低於上月償還本金,等額本息模式則無此約束。

想想這個邏輯也有道理,如果真的“月供不變”,那麼等額本金模式下提前還款後,後續每個月償還的本金都會比新做貸款的償還的本金多,相當於後續每個月都在提前還款,後續每個月月供本金就不能稱為“等額”了。
我們下面先寫個求解首月月供的函式,以及透過縮短年限逼近上月月供總額和月供本金的函式。而後計算“月供不變,年限縮短”模式下節省的具體利息。

def getFirstPaid(months, principal, rate, capitalAveraged):
 month_rate = rate / 12
 monthly_capital = principal / months
 monthly_payment = principal * month_rate * (1 + month_rate) ** months / ((1 + month_rate) ** months - 1)
    interests1 = principal * month_rate
 if capitalAveraged:
 return monthly_capital + interests1, monthly_capital
 else:
 return monthly_payment, monthly_payment - interests1
def getLeftMonths(leftMonthsMax, capitalPaidMax, paidMax, leftPrincipal, rate, capitalAveraged):
 lastPaid, lastCapitalPaid, lastMonths = 0, 0, 0
 for i in range(leftMonthsMax, 1, -1):
        paid, capitalPaid = getFirstPaid(i, leftPrincipal, rate, capitalAveraged)
 if paid > paidMax or (capitalAveraged and capitalPaid > capitalPaidMax):
 return lastMonths, lastPaid, lastCapitalPaid
 else:
 lastPaid, lastCapitalPaid, lastMonths = paid, capitalPaid, i
def extraPaidWithFixedPaid(months, principal, rate,
 capitalAveraged, extraPaidList: list):
    capitals, interests, total_payment = normalPaid(
        months, principal, rate, capitalAveraged)
 extraPaidList.sort(key=lambda x: x[0])
 originCapital, originInterests, originTotal = capitals.copy(), interests.copy(), total_payment.copy()
 left_principal = [0] * months
 left_principal[0] = principal
 for x in range(0, months):
 if x < months - 1:
 left_principal[x + 1] = left_principal[x] - capitals[x]
    def normalPaidOffset(left_months, principal, rate,
 capitalAveraged, offset, left_months2):
 month_rate = rate / 12
 monthly_capital = left_principal[offset] / left_months
 monthly_payment = left_principal[offset] * month_rate * (1 + month_rate) ** left_months / ((1 + month_rate) ** left_months - 1)
 for i in range(0, left_months):
 interests[offset + i] = left_principal[offset + i] * month_rate
 if capitalAveraged:
 capitals[offset + i] = monthly_capital
 total_payment[offset + i] = monthly_capital + interests[offset + i]
 else:
 total_payment[offset + i] = monthly_payment
 capitals[offset + i] = total_payment[offset + i] - interests[offset + i]
 if i == 0:
 print("次月還款 %.2f" % total_payment[offset + i])
 if offset + i + 1 < months:
 left_principal[offset + i + 1] = left_principal[offset + i] - capitals[offset + i]
 for i in range(left_months, left_months2):
 interests[offset + i] = 0
 capitals[offset + i] = 0
 total_payment[offset + i] = 0
 return
 realMonth = months
 for x, y in extraPaidList:
 capitalParam = capitals[x]
        capitals[x] = capitals[x] + y
 left_principal[x + 1] = left_principal[x] - capitals[x]
 total_payment[x] = capitals[x] + interests[x]
 maxMonth, maxPaid, maxPaidCapital = getLeftMonths(months - x - 1, capitalParam, total_payment[x - 1], left_principal[x + 1], rate, capitalAveraged)
 normalPaidOffset(maxMonth, left_principal[x + 1], rate, capitalAveraged, x + 1, months - x - 1)
 realMonth = x + 1 + maxMonth
 print("當月需還 %.2f 剩餘本金 %.2f 下月需還:%.2f  原本剩餘賬期:%d,當前剩餘賬期:%d, 賬期縮短:%d" %(total_payment[x], left_principal[x + 1],total_payment[x + 1], months - x - 1,maxMonth, months - x - 1 - maxMonth))
 printStatistics(originCapital, originInterests, originTotal, months)
 print("")
 printStatistics(capitals, interests, total_payment, realMonth)
 print("節省利息 %.2f" % (np.sum(originInterests) - np.sum(interests)))
 return capitals, interests, total_payment, originTotal, originInterests
a, b, c, d, e = extraPaidWithFixedPaid(12 * 20, 875000, 0.049, True, [(13, 100000)])
drawDiagramExtraPaid(12 * 20, a, b, d, e)
drawTableExtraPaid(12 * 20, a, b, c, d, e)[10:20]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然
a, b, c, d, e = extraPaidWithFixedPaid(12 * 20, 875000, 0.049, False, [(13, 100000)])
drawDiagramExtraPaid(12 * 20, a, b, d, e)
drawTableExtraPaid(12 * 20, a, b, c, d, e)[10:20]
還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然還在頭疼每月房貸還款?這個房貸計算機讓你一目瞭然

可以看出,雖然縮短年限的本質也是重新做一次貸款,但確實可以節省很多利息。

小結

本文初稿寫於華為雲AI-Gallery貸款計算器-從原理、公式到提前還款和視覺化,透過頁面進入CodeLab可以直接在介面上調整引數進行房貸利息、提前還款等相關計算,計算過程原理直觀,配合視覺化方便理解,歡迎開發者前往體驗。

整篇文章帶大家瞭解了不同房貸貸款方式的差異,以及對房貸利息計算、提前還款的原理做了較為細緻的剖析和資料視覺化。後續在面對貸款利息計算的問題時,可以直面原理、心中有數、臨危不慌。

參考資料

[1]用Python深度解讀房貸利率

[2]為什麼買房貸款,最好選擇等額本金?

[3]杭州房小團微信小程式-貸款計算

[4]杭州房小團微信小程式-提前還款

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章