Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: Using recursion to find lowest fixed monthly payment in multiples of 10

I am getting 11 or 12 of 15 correct in a Python course on edX.org each time I submit, but not getting much help from anyone in the discussions because no one can really post any code there (not really helpful) and there doesn't seem to be any available support staff to speak with from the course, which I would pay for, so I am posting here. I was about to pay someone to tutor me but no one is available right now, and I am under some pressure to get this course done by December for my job.

This is the assignment:

Now write a program that calculates the minimum fixed monthly payment needed in order pay off a credit card balance within 12 months. By a fixed monthly payment, we mean a single number which does not change each month, but instead is a constant amount that will be paid each month.

In this problem, we will not be dealing with a minimum monthly payment rate.

The following variables contain values as described below:

balance - the outstanding balance on the credit card

annualInterestRate - annual interest rate as a decimal

The program should print out one line: the lowest monthly payment that will pay off all debt in under 1 year, for example:

Lowest Payment: 180
Assume that the interest is compounded monthly according to the balance at the end of the month (after the payment for that month is made). The monthly payment must be a multiple of $10 and is the same for all months. Notice that it is possible for the balance to become negative using this payment scheme, which is okay. A summary of the required math is found below:

Monthly interest rate = (Annual interest rate) / 12.0
Monthly unpaid balance = (Previous balance) - (Minimum fixed monthly payment)
Updated balance each month = (Monthly unpaid balance) + (Monthly interest rate x Monthly unpaid balance)

This is my code:

#! /usr/bin/python3.6

from math import ceil

def roundup(x):
    return int(ceil(x / 10.0) * 10)

def getFixedPayment(balance,annualInterestRate,counter=12):

    totalLoan = balance + (balance*annualInterestRate)
    monthlyPayment = roundup(totalLoan/12.0)
    newBalance = totalLoan - monthlyPayment
    if counter < 12:
        newPayment = newBalance / counter + 1
    else:
        newPayment = newBalance / counter

    if counter == 1:
        return roundup(newPayment/12)
    else:
        return getFixedPayment(balance,annualInterestRate,counter-1)


#balance = 3329
#annualInterestRate = 0.2
print('Lowest Payment: ' + str(getFixedPayment(balance,annualInterestRate)))

Here are the test results: (I have here all 15, so you might be able to identify a pattern I can't see. The ones marked "ERROR" are the ones I got incorrect)

Test Case 1
balance = 3329; annualInterestRate = 0.2
Output:
Lowest Payment: 310

Test Case 2
balance = 4773; annualInterestRate = 0.2
Output:
Lowest Payment: 440

Test Case 3
balance = 3926; annualInterestRate = 0.2
Output:
Lowest Payment: 360

Randomized Test Case 1
balance = 265; annualInterestRate = 0.18
Output:
Lowest Payment: 30

Randomized Test Case 2
balance = 263; annualInterestRate = 0.18
Output:
Lowest Payment: 30

Randomized Test Case 3
balance = 317; annualInterestRate = 0.25
Output:
Lowest Payment: 30

Randomized Test Case 4
balance = 720; annualInterestRate = 0.2
Output:
Lowest Payment: 70

Randomized Test Case 5
balance = 4284; annualInterestRate = 0.2
Output:
Lowest Payment: 400

Randomized Test Case 6
balance = 3834; annualInterestRate = 0.15
Your output:
Lowest Payment: 340


*** ERROR: Expected Lowest Payment: 350

, but got Lowest Payment: 340

 ***
Correct output:
Lowest Payment: 350

Randomized Test Case 7
balance = 3045; annualInterestRate = 0.18
Output:
Lowest Payment: 280

Randomized Test Case 8
balance = 4461; annualInterestRate = 0.2
Output:
Lowest Payment: 410

Randomized Test Case 9
balance = 4657; annualInterestRate = 0.04
Your output:
Lowest Payment: 370


*** ERROR: Expected Lowest Payment: 400

, but got Lowest Payment: 370

 ***
Correct output:
Lowest Payment: 400

Randomized Test Case 10
balance = 3395; annualInterestRate = 0.2
Your output:
Lowest Payment: 320


*** ERROR: Expected Lowest Payment: 310

, but got Lowest Payment: 320

 ***
Correct output:
Lowest Payment: 310

Randomized Test Case 11
balance = 4045; annualInterestRate = 0.15
Your output:
Lowest Payment: 360


*** ERROR: Expected Lowest Payment: 370

, but got Lowest Payment: 360

 ***
Correct output:
Lowest Payment: 370

Randomized Test Case 12
balance = 3963; annualInterestRate = 0.18
Output:
Lowest Payment: 360
like image 789
Debug255 Avatar asked Nov 19 '17 08:11

Debug255


Video Answer


1 Answers

Although the function is recursive (it calls itself), it does so in a pointless, ineffective way.

Consider what happens for any value of the counter when greater than 1:

def getFixedPayment(balance, annualInterestRate, counter=12):
    totalLoan = balance + (balance*annualInterestRate)
    monthlyPayment = roundup(totalLoan/12.0)
    newBalance = totalLoan - monthlyPayment

    if counter < 12:
        newPayment = newBalance / counter + 1
    else:
        newPayment = newBalance / counter

    if counter == 1:
        return roundup(newPayment/12)
    else:
        return getFixedPayment(balance,annualInterestRate,counter-1)
        #                                                 ^^^^^^^^^
        #                                                 the only change!

When counter > 1, the function "does some calculations", but it doesn't matter, because in the end it just calls itself with counter - 1. So for a starting value of counter = 12, the function will repeatedly call itself 11 times for nothing. As such it reduces to this:

def getFixedPayment(balance, annualInterestRate):
    totalLoan = balance + (balance*annualInterestRate)
    monthlyPayment = roundup(totalLoan/12.0)
    newBalance = totalLoan - monthlyPayment

    newPayment = newBalance / counter + 1

    return roundup(newPayment/12)

Can this possibly give correct answer? Not likely.

Consider how the repayment works. Let's take a simpler example of repaying something in 3 steps, and consider this alternative notation so it's simpler to write:

  • Let's call T the total amount to pay today
  • Let's call r the multiplier of the annual interest rate, that is for a 4% annual interest rate, this value will be 1.04, so we can get the amount that needs to be paid by T * r.
  • Let's call x the target fixed monthly payment

After we pay x this month, how much will be left to repay?

(T - x) * r

That is, we pay x this month, the remaining amount is T - x, and as per the description we need to multiply with the annual interest rate.

Next month, we pay x again, so what's left to repay will be:

((T - x) * r - x) * r

In the third month we make the last payment of x.

The target of the exercise is to find the x such that:

((T - x) * r - x) * r - x <= 0

Let's reorganize this equation to find the value of x:

((T - x) * r - x) * r <= x

(T - x) * r - x <= x / r

(T - x) * r <= x / r + x

T - x <= x / r / r + x / r

T <= x / r / r + x / r + x

T <= x * (1 / r / r + 1 / r + 1)

T / (1 / r / r + 1 / r + 1) <= x

In this example we repay in 3 months. You can see how this formula changes by adding one month:

T / (1 / r / r / r + 1 / r / r + 1 / r + 1) <= x

That is, given T / m, adding one month means T / (m / r + 1).

Now that looks like a recursive logic we can use.

Without completely spoiling the exercise for you, here's a template of the solution where you just need to figure out the correct values for the .... Good luck!

def getFixedPayment(balance, annual_interest_rate, counter=12, interest=...):

    if counter == 1:
        return roundup(balance / interest)

    monthly_interest_rate = annual_interest_rate / 12
    r = 1 + monthly_interest_rate

    return getFixedPayment(balance, annual_interest_rate, counter - 1, ...)

And here are some doctests to verify your solution. If your program is in a file called calc.py, you can run the doctests with python -mdoctest calc.py. It will print a summary of the failing test cases if any, or it will print nothing if all good.

def getFixedPayment(balance, annual_interest_rate, counter=12, interest=...):
    """
    >>> getFixedPayment(3329, 0.2)
    310

    >>> getFixedPayment(4773, 0.2)
    440

    >>> getFixedPayment(3926, 0.2)
    360

    >>> getFixedPayment(265, 0.18)
    30

    >>> getFixedPayment(263, 0.18)
    30

    >>> getFixedPayment(317, 0.25)
    30

    >>> getFixedPayment(720, 0.2)
    70

    >>> getFixedPayment(4284, 0.2)
    400

    >>> getFixedPayment(3834, 0.15)
    350

    >>> getFixedPayment(3045, 0.18)
    280

    >>> getFixedPayment(4461, 0.2)
    410

    >>> getFixedPayment(4657, 0.04)
    400

    >>> getFixedPayment(3395, 0.2)
    310

    >>> getFixedPayment(4045, 0.15)
    370

    >>> getFixedPayment(3963, 0.18)
    360

    """

    if counter == 1:
        return roundup(balance / interest)

    monthly_interest_rate = annual_interest_rate / 12
    r = 1 + monthly_interest_rate

    return getFixedPayment(balance, annual_interest_rate, counter - 1, ...)
like image 115
janos Avatar answered Oct 05 '22 09:10

janos