动机
网络上有很多贷款计算器,包括提前还款、利率变化等。但大多数只能输入一次利率变化或提前还款。然而,实际中,可能会经过多次LPR调整,以及多次部分还款。用excel计算过于复杂,所以码了代码用以具体分析。在对比结果的过程中,发现了很多有意思的现象,比如提前还款时利息的计算,下面会提到。
理论
-
等额本息还款公式
月还款额 = 本金 × ( 1 + 月利率 ) 期数 × 期数 ( 1 + 月利率 ) 期数 + 1 月还款额=\frac{本金\times(1+月利率)^{期数} \times 期数}{(1+月利率)^{期数} +1} 月还款额=(1+月利率)期数+1本金×(1+月利率)期数×期数
注:期数单位为月 -
月还款利息:
月还款利息 = 剩余本金 ∗ 月利率 月还款利息=剩余本金*月利率 月还款利息=剩余本金∗月利率 -
月还款本金
月还款本金 = 月还款额 − 月还款利息 月还款本金=月还款额-月还款利息 月还款本金=月还款额−月还款利息 -
关于提前还款:提前还款利息计算是分段的,按照日利率计算,且每月按照30天计算,日期计算计头不计尾。例如,上期还款日为2/24,提前还款日为3/9,那么:
当期应还利息 = ( 原剩余本金 × 15 + 提前还款后剩余本金 × 15 ) × 日利率 当期应还利息= (原剩余本金 \times 15+提前还款后剩余本金 \times 15) \times 日利率 当期应还利息=(原剩余本金×15+提前还款后剩余本金×15)×日利率
其中,2月份按照30天计算,则从24日到次月9日(提前还款日)共15天(日期区间[24,30]+[1,9)),到下次还款日15天。根据央行规定,
日利率 = 年利率 / 360 = 月利率 / 12 日利率=年利率/360=月利率/12 日利率=年利率/360=月利率/12 -
关于提前还款扩展:包括三种,a)期限不变,月供减少;b)月供不变,期限变短;c)两者都减少。大部分银行只支持前两种。其中,月供不变,期限变短后计算公式可以从月还款额计算公式轻易推导出来:
新的还款期限 = l o g ( 月还款额 / ( 月还款额 − 剩余本金 ∗ 月利率 ) ) l o g ( 1 + 月利率 ) 新的还款期限=\frac{log(月还款额/(月还款额-剩余本金*月利率))}{log(1+月利率)} 新的还款期限=log(1+月利率)log(月还款额/(月还款额−剩余本金∗月利率))
公式推导网上很多资料,不赘述。
代码
话不多说,上代码
def cal_basic(monthly_interest_rate, total_months, capital):
# 计算基础信息,包括月供、累计还款额,累计利息
# 参数分别为月利率,总期数,本金
total_per_month = capital * (1+monthly_interest_rate)** total_months * monthly_interest_rate / ((1+monthly_interest_rate)**total_months-1) # 月供
cumulative_pay = total_months * capital * (1+monthly_interest_rate)** total_months * monthly_interest_rate / ((1+monthly_interest_rate)**total_months-1) # 累计还款,包括本金和利息
cumulative_interest = cumulative_pay - capital # 累计还利息
return (round(total_per_month, val_decimal_num), round(cumulative_pay,val_decimal_num), round(cumulative_interest,val_decimal_num))
def cal_per_month_by_left_capital(monthly_interest_rate, left_capital, total_per_month):
# 逐月计算月还款利息等信息,包括每月还款利息、每月还款本金、剩余本金
# 输入参数为月还款利息,剩余本金、月供
interest_month = left_capital * monthly_interest_rate
capital_month = total_per_month - interest_month
if capital_month > left_capital:
# 最后一个月,本金可能小于基于月供计算的数
capital_month = left_capital
# print(left_capital, capital_month, left_capital - capital_month)
return (round(interest_month, val_decimal_num), round(capital_month, val_decimal_num), round(left_capital - capital_month, val_decimal_num))
以上两个函数主要用于计算月供、每月还款利息、每月还款本金等信息。
下面是几个辅助函数,功能见说明
def cal_delta_months(start_date, event_happen_date):
# 计算某个时间发生的日期相较于起始日期的月份偏移数
delta_year = event_happen_date.year - start_date.year
delta_month = event_happen_date.month - start_date.month
delta_day = event_happen_date.day - start_date.day
res = delta_year * 12 + delta_month
if delta_day < 0:
res -= 1
return res
def next_month(current_date):
# 获取下个月的datetime对象
year = current_date.year
month = current_date.month
if month == 12:
year += 1
month = 1
else:
month += 1
return datetime(year, month, current_date.day)
def cal_prepapment_interest(original_captical, prepay_amount, monthly_interest_rate, last_pay_date, prepay_date):
# 前提:上次付款日和提前还款日差值不超过30天
# 计算提前还款当月应还利息
if prepay_date.month == last_pay_date.month:
# 同一个月
duration_before = prepay_date.day - last_pay_date.day
duration_after = 30 - last_pay_date.day + last_pay_date.day # 提前还款后,下次还款日相同 = 30-duration_before
else:
# 不同月,则一个月按照30天计算
duration_before = 30 - last_pay_date.day + prepay_date.day
duration_after = last_pay_date.day - prepay_date.day # 日期算头不算尾 = 30-duration_before
# print(duration_before, duration_after)
return round((original_captical * duration_before + (original_captical - prepay_amount) * duration_after) * monthly_interest_rate / 30, val_decimal_num)
下面函数为总体函数。
def view_all_pay_info(monthly_interest_rate, total_months, capital, start_date, change_event_list):
# 主函数。根据起始月利息,期数、本金、开始还款日期、事件发生列表等,生成每月还款信息表
current_monthly_interest_rate = monthly_interest_rate
m_pay_fixed,_,_ = cal_basic(monthly_interest_rate, total_months, capital) # 总的还款信息:月供、累计还款、累计还利息
current_date = start_date
full_info_list = []
total_left_capital = capital # 剩余本金
# 计算事件发生改变的月
has_event_month_indexes = []
for event in change_event_list:
has_event_month_indexes.append(cal_delta_months(start_date, event['happen_date']) + 1 + 1) # +1是从第一个月开始计算,再+1是事件生效月
event_index = 0
print(has_event_month_indexes)
for m in range(1, total_months+1):
if m > total_months:
# 总还款时间有可能变化
break
# 处理事件
description = ''
changed_interest = 0 # 如果提前还款,可能会导致当月利息变化
if m in has_event_month_indexes:
for n in range(has_event_month_indexes.count(m)):
# 可能发生多个事件
event = change_event_list[event_index]
event_index += 1
if event['event_type'] == 0:
# 利率改变
current_monthly_interest_rate = round(event['new_yearly_interest_rate'] / 12, rate_decimal_num) # 保留x位小数
m_pay_fixed,_,_ = cal_basic(current_monthly_interest_rate, total_months-m+1, total_left_capital) # 更新总还款信息
description += '利率变化:' + str(current_monthly_interest_rate) + ' '
elif event['event_type'] == 1:
# 提前还款
changed_interest = cal_prepapment_interest(total_left_capital, event['amount'], current_monthly_interest_rate, last_date, event['happen_date'])
total_left_capital -= event['amount']
if event['prepay_type'] == 0:
# 期限不变,月供减少
m_pay_fixed,_,_ = cal_basic(current_monthly_interest_rate, total_months-m+1, total_left_capital) # 更新总还款信息
description += '提前还款:' + str(event['amount'] / 10000) + '万 '
elif event['prepay_type'] == 1:
# 期限变短,月供不变
# 计算期限
new_duration = prepayment_decrease_duration(m_pay_fixed, current_monthly_interest_rate, total_left_capital)
print(new_duration, m)
total_months = new_duration + m -1
description += '提前还款:' + str(event['amount'] / 10000) + '万 '
else:
# 月供减少,同时期限变短
total_months = event['new_total_months']
m_pay_fixed,_,_ = cal_basic(current_monthly_interest_rate, total_months-m+1, total_left_capital) # 更新总还款信息
description += '提前还款:' + str(event['amount'] / 10000) + '万 '
m_interest, m_capital, total_left_capital = cal_per_month_by_left_capital(current_monthly_interest_rate, total_left_capital, m_pay_fixed) # 每月还利息、每月还本金、剩余本金
m_pay_total = round(m_pay_fixed, val_decimal_num) # 当月还款总额
if changed_interest != 0:
# 利息变化
m_interest = changed_interest
m_pay_total = m_capital + changed_interest
full_info_list.append((current_date, m, m_pay_total, m_interest, m_capital, total_left_capital, description))
last_date = current_date
current_date = next_month(current_date)
full_info_list_df = pd.DataFrame(full_info_list, columns=['日期', '月数', '月供', '还利息', '还本金', '剩余本金', '事件描述'])
return full_info_list_df
输入参数,可以计算输出相应结果。
# 基础参数
capital = 3040000 # 本金
months = 25 * 12 # 月数
start_date = datetime(2021,7,24)
year_interest_rate = 5.2 / 100
change_event_list = [] # 注意,这里按照事件的发生时间由远及近
change_event_list.append(
{
'happen_date': datetime(2022,7,1),
'event_type': 0, # 0-->利率改变,1-->提前还款
'new_yearly_interest_rate': 0.05
})
change_event_list.append(
{
'happen_date': datetime(2023,3,9),
'event_type': 1, # 0-->利率改变,1-->提前还款
'amount': 200000,
'prepay_type': 0, # 0:期限不变,月供变少;1月供不变,期限变短; 2月供减少,同时期限变短
})
change_event_list.append(
{
'happen_date': datetime(2023,6,24),
'event_type': 0, # 0-->利率改变,1-->提前还款
'new_yearly_interest_rate': 0.0475
})
change_event_list.append(
{
'happen_date': datetime(2023,7, 22),
'event_type': 1, # 0-->利率改变,1-->提前还款
'amount': 200000,
'prepay_type': 1, # 0:期限不变,月供变少;1月供不变,期限变短; 2月供减少,同时期限变短
})
rate_decimal_num = 8 # 利率精度
val_decimal_num = 2 # 数值精度
res = view_all_pay_info(round(year_interest_rate / 12, rate_decimal_num), months, capital, start_date, change_event_list)
res.to_csv('house_loan.csv', index=False)
可以在change_event_list
列表中添加利率变化、提前还款等利息。
结果示例
由于已经拿到了DataFrame格式的所有信息,可以进行各种对比分析。图形化分析部分不做展开,仅显示下表格。其中数据已经与真实数据对比准确无误。