Search code examples
pythonnumpyrate

Getting negative rate for specific set of values in numpy rate function


I am new to numpy and using numpy.rate() to calculate APR for flat interest rate monthly payment loans.

no_of_month = 24
payment = 8584
loan_amount = 50000
apr = rate(no_of_month,-payment,loan_amount,0.0) *12

The apr is calculated to be -22.816 but the actual value should be 2.0102(calculated in LibreOffice Calc)

For no_of_month = 23 the apr is 2.00079 but for no_of_month = 24 the apr is -22.816

Following are my current guess on why this happens (They may be wrong)

  • Because the duration touches multiple of 12 (another whole year)
  • Some limitation to in numpy.rate for certain range

I can't find any related resource for either.

What is the root cause and how to fix this?

Range for the values

no_of_month - 1 to 36
payment - 1000 to 1000000
loan_amount - 10000 to 10000000

All combination is possible


Solution

  • np.rate returns the interest rate that solves a polynomial equation with x**24. This has 24 solutions, some of which may be duplicated, some of which may be complex. In the case of this particular data:

    pv = 50000
    payment = 8584 
    mpr= np.rate(24, -payment, pv, 0.0)
    np.pv(mpr, 24, payment)
    # -49999.999999789325  This represents the 50000 pv 
    mpr
    # -1.901406995298687  #  mpr = -190.1% per month!
    
    mpr1 = np.rate(24, -payment, pv, 0.0, guess = .15)
    # guess lets you change the starting point for the search
    mpr1
    # 0.16750654293672343  # mar = 16.8% per month
    np.pv(mpr1, 24, payment)
    # -49999.99999999999
    
    def apr(mpr, periods = 12):
        """  apr is ( 1 + monthly_rate ) ** 12 - 1 """
        return (1+mpr)**periods-1
    
    apr(apr)
    # -0.7122263079633477  apr = -71.2%
    
    apr(mpr1)
    # 5.4137477809069345  apr of 541.4%
    

    i.e. Both 16.8% and -190.1% are mathematically correct solutions to the equations. -190.1% doesn't make much sense in a financial context.

    It's perhaps easier to understand for two periods.

    loan_amount = 10
    payment = 6
    n_periods = 2
    
    Solve 10 - 6r -6r**2
    r = (6 +-sqrt(36-4*(-6)*10))/(2*-6)
    r = -1.8844373105 and 0.8844373105
    
    r = 1/(1+i) where i is the interest rate
    i = 1/r - 1
    
    r = -1.8844373105 
    1/r-1
    # -1.5306623862879625
    
    r1 = 0.8844373105
    1/r1-1
    # 0.13066238627435212   
    
    mpr = np.rate(2, -payment, loan_amount, 0.0)
    mpr
    # 0.13066238629183413
    
    mpr1 = np.rate(2, -payment, loan_amount, 0.0, guess = -1.5)
    mpr1
    # -1.5306623862918336
    

    There are two rates that numpy will solve for in this case which come from the two roots to the quadratic equation.

    This probably doesn't help but does explain why np.rate (and np.irr) can solve to unexpected answers.

    Edit:

    I realised that if r = 1/(1 + interest_rate) there will only be one real positive solution for r. This is the solution that generally has the most business meaning.

    import numpy as np
    """
        To simplify the analysis let 
            r = 1 / ( 1 + interest_rate )
            p = periodic payments
            n = number of periods
        then:
            pv = loan - p*r - p*r**2 - ... -p*r**n
            dpv/dr =  -p -2*p*r - ... -p*n*r**(n-1)
            If r > 0 and p > 0 dpv/dr is negative
            Therefore there is at most one solution to pv == 0 for r > 0.
            pv == loan when r == 0
            For large r -p*r**n will dominate and pv will be negative.
            Therefore there will be one positive real solution to pv == 0
    """
    
    def polynomial_from(nper, loan, pay): 
        """ Create numpy array to represent the polynomial """
        return np.array([-pay]*nper+[loan])
    
    # np.roots returns one root per nper.  Filter to real roots only.
    def real_roots(poly):
        roots_ = np.roots(poly)
        return roots_[np.isclose(roots_.imag, 0)].real
        # return the real part of the roots- with a zero imaginary part
    
    def feasible_rate(nper, loan, pay):
        poly = polynomial_from(nper, loan, pay)
        reals = real_roots(poly)
        r = reals[reals>0][0]   # r is the real root > 0
        return 1/r - 1
    
    def apr(int_rate, nperiods = 12):
        return ( 1 + int_rate ) ** nperiods - 1
    
    mpr = feasible_rate( 24, 50000, 8584 )
    print( 'Monthly rate: {:%}, Annual Rate: {:%}'.format(mpr, apr(mpr)) )
    # Monthly rate: 16.750654%, Annual Rate: 541.374778%