在前面贷款基础知识之等本等息中我们看到,对于等本等息或者信用卡分期来说,名义利率(也就是纸面上列出的数字)并不是真实的利率,真实的利率比名义利率是要高很多的。那么在实际生活中,如何才能不轻易上当,识破各种机构的隐藏陷阱呢?下面介绍几种方法。

1. Excel RATE 函数

仍然举贷款基础知识之等本等息中的例子,贷款 10 万元,分 12 期偿还,每期还款 8833.33 元,名义利率 6%。RATE 公式的语法是:

RATE(nper, pmt, pv, [fv], [type], [guess])

其中,nper 是付款总期数;pmt 是每期的付款金额;pv 是现值;后面的 fvtypeguess 都是可选项,一般忽略即可。代入公式为:RATE(12, -8833.33, 100000) = 0.908% 即为每一期的利率,乘以 12 得到实际的年利率为 10.896%。

需要注意的一点是,因为每个月的 $8833.33$ 元是支出,所以是负数。另外,关于 RATE 函数的更详细解释可以参考官方文档

2. 几十行 Pythond 代码

自己写代码时就不用那么严谨了,对于收入和支出的正负号不用那么讲究。粗略写了下,共 23 行代码。因为每月还款额都是一样的,所以用二分法去试利率,一般十多二十次都能收敛,对于未收敛的,直接返回 -1。具体代码如下所示:

def rate(nper, pmt, pv):
    def _pmt(_rate):
        x = (1 + _rate)**nper
        return pv * _rate * x / (x - 1)
    max_iter_times = 64
    lr, hr, i = 0, 1.0, 0
    while _pmt(hr) < pmt and i < max_iter_times:
        lr, hr, i = hr, 2*hr, i+1
    if i == max_iter_times:
        return -1
    i = 0
    while lr < hr and i < max_iter_times:
        mr = (lr + hr) / 2.0
        x = _pmt(mr)
        if abs(x - pmt) < 1e-2:
            return mr
        if x < pmt:
            lr = mr
        else:
            hr = mr
        i += 1
    if i == max_iter_times:
        return -1

对于之前的例子,调用 rate(12, 8833.33, 100000.0) 得到结果 0.00908,与 Excel RATE 公式计算出来的结果是一样的。

3. 改进的 Python 代码

虽然二分法在实际中是一个特别高效的算法,但对于这个问题,仍然不是最高效的,是时候祭出牛顿法了。

最早接触牛顿法是在求开方这个问题上。在此,插个题外话,不得不提 Quake III 里的一段神代码,如下。这段代码的作用是计算浮点数开方后的倒数,也即 $1/\sqrt{x}$,效率极其的高,只用了一次迭代就得到了很高的精度。想更深入地了解这段代码的话可以参考 Fast Inverse Square Root 这篇论文。

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck? 
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//  y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

小插曲之后继续回来,要求一个数的平方根,同样可以采用二分法,不断猜测并缩小范围,最终找到结果,但是这种方法的效率太低,需要迭代好多次。牛顿法能更高效地解决这个问题,基本原理是这样的,对于已知的曲线方程,比如 $f(x)$,要求零点,则可以随机选一个点,比如 $x_0$,然后在 $x_0$ 点作一条切线,切线与 $x$ 轴的交点记为 $x_1$,则满足方程 $f(x_0)-0= f’(x_0)\cdot(x_0-x_1)$,则 $x_1 = x_0 - f(x_0)/f’(x_0)$。依此公式迭代即可。为了更好地理解这个过程,可以看下面的动画:

当然,牛顿法也是有一定适用范围的,具体可以参考 Wikipedia知乎上的介绍

继续回到本文的问题,也就是求实际的利率,我们可以利用每个月的还款额来反推。从贷款基础知识之等额本息中我们知道等额本息的还款公式是:

$$ pmt = \frac{pv\cdot rate\cdot(1+rate)^{nper}}{(1+rate)^{nper}-1}$$

将待求的利率记为 $x$,同时,不妨记

$$f(x) = \frac{pv\cdot x\cdot(1+x)^{nper}}{(1+x)^{nper}-1} - pmt = \frac{(pv\cdot x - pmt)\cdot(1+x)^{nper} + pmt}{(1+x)^{nper}-1}$$

由于 $f(x)$ 的分母部分恒大于 $0$,对于求零值没有影响,因此可以忽略,$f(x)$ 由此可以简化为:

$$ f(x) = (pv\cdot x - pmt)\cdot(1+x)^{nper} + pmt$$

对 $x$ 求导得到 $f’(x)$,不妨记为 $g(x)$,因此:

$$g(x) = f’(x) = (1+x)^{nper - 1}\cdot(pv\cdot(1 + x + x\cdot nper) - pmt\cdot nper)$$

根据前面的分析,迭代更新的公式为:

$$ x_{n+1} = x_n - f(x)/g(x)$$

因此,不难写出 Python 代码,如下所示:

def rate_newton(nper, pmt, pv, guess=0.3/12):
    f = lambda x: (pv*x - pmt)*(1 + x)**nper + pmt
    g = lambda x: (1 + x)**(nper-1) * (pv*(1 + x + x * nper) - pmt * nper)
    x = guess
    i, max_iter_times = 0, 32
    while i < max_iter_times:
        fx = f(x)
        if abs(fx) < 1e-2:
            return x
        x -= fx / g(x)
        if x < 0: # in case
            x = -x/2
        i += 1
    return -1   # Didn't converge

对于前面的例子,调用 rate_newton(12, 8833.33, 100000) 同样可以得到结果 0.00908,而且迭代次数比二分法要少很多。


本文介绍了几种计算真实利率的方法,Excel 的 RATE 函数是最简单的,如果想探究背后的计算方法,则可以自己写代码搞定,计算方法也介绍了二分和牛顿法两种,这两种算法思想也是有非常广泛的应用,最好都能熟练掌握。