# For a given balance and an annual interest rate, calculate the minimum fixed monthly payment to pay off debt in a year

Interest is compounded monthly. Payment doesn't vary from month to month.

Version 1

balance = 999999
annualInterestRate = 0.18
mRate = annualInterestRate/12
high = (((mRate+1)**12)*balance)/12
low = balance/12
guessed = False

def balanceLeft(balance,mRate,minPayment):
monthsLeft = 12
while monthsLeft > 0:
unpaidBalance =  balance - minPayment
interest = mRate * unpaidBalance
balance = unpaidBalance
balance += interest
monthsLeft -= 1
return balance

while guessed == False:
minPayment = (high + low) / 2
if round(balanceLeft(balance,mRate,minPayment),2) < 0:
high = minPayment
elif round(balanceLeft(balance,mRate,minPayment),2)> 0:
low = minPayment
else:
if abs(round(balanceLeft(balance,mRate,minPayment),2) - 0) < 0.01:
guessed = True

print('Lowest Payment: ',end='')
print(round(minPayment,2))


Version 2

annualInterestRate = 0.18
rate = annualInterestRate / 12
monthsLeftr = 12
xCoefficent = 1 + rate
ConstantTerm = 1 + rate
while monthsLeftr > 1:
xCoefficent = (xCoefficent + 1) * ConstantTerm
monthsLeftr -= 1

balance = 999999
monthsLeft = 12
while monthsLeft > 0:
balance = balance * ConstantTerm
monthsLeft -= 1
minPayment = balance / xCoefficent

print('Lowest Payment: ', end="")


I would like a comparative and overall review, please!

• Given any 3 of payment amount, term, interest rate, and initial principal, the fourth one can be calculated algebraically, except for interest rate, which requires a search. C code for all these at dragonreef.net/ammo.c. Sep 28 '16 at 16:59

• use a consistent style: space after a comma, or not; spaces around operators, or not; even two spaces sometimes. Python has an official style guide called PEP 8 which is worth a read, you’ll understand why the syntax highlight here is colouring ConstantTerm in light blue.
• use functions to parametrize values. As it stand it is impossible to reuse any of that code to perform computations with other rates, initial lending, or even various number of years.
• use the if __name__ == '__main__': clause to wrap your top-level code.
• use a for loop to perform the same computation a given number of time. e.g.:

for _ in range(monthsLeft):
balance = balance * ConstantTerm

• use constants for magic numbers: MONTHS_IN_A_YEAR = 12

A first and quick rewrite could be:

MONTHS_IN_A_YEAR = 12

def balance_left(balance, monthly_rate, monthly_payment):
for _ in range(MONTHS_IN_A_YEAR):
unpaid_balance =  balance - monthly_payment
interest = monthly_rate * unpaid_balance
balance = unpaid_balance
balance += interest
return balance

def minimal_monthly_payment(balance, annual_interests):
monthly_rate = annual_interest / MONTHS_IN_A_YEAR
lower_bound = balance / MONTHS_IN_A_YEAR
higher_bound = (monthly_rate + 1)**MONTHS_IN_A_YEAR * lower_bound
guessed = False

while not guessed:
monthly_payment = (higher_bound + lower_bound) / 2
yearly_balance = balance_left(balance, monthly_rate, monthly_payment)
rounded_balance = round(yearly_balance, 2)
if rounded_balance < 0:
higher_bound = monthly_payment
elif rounded_balance > 0:
lower_bound = monthly_payment
else:
if abs(rounded_balance - 0) < 0.01:
guessed = True
return monthly_payment

if __name__ == '__main__':
minimal_cost = minimal_monthly_payment(999999, 0.18)
print('Lowest Payment:', round(minimal_cost, 2))

MONTHS_IN_A_YEAR = 12

def annual_rate_coefficient(monthly_interests_rate):
coefficient = monthly_interests_rate
for _ in range(MONTHS_IN_A_YEAR - 1):
coefficient = (coefficient + 1) * monthly_interests_rate

def minimal_monthly_payment(balance, annual_interests):
monthly_interests_rate = 1 + annual_interests / MONTHS_IN_A_YEAR
for _ in range(MONTHS_IN_A_YEAR):
balance = balance * monthly_interests_rate
return = balance / annual_rate_coefficient(monthly_interests_rate)

if __name__ == '__main__':
minimal_cost = minimal_monthly_payment(999999, 0.18)
print('Lowest Payment:', round(minimal_cost, 2))


# Version 1

You can rearange the maths in balance_left to make it more efficient:

balance = unpaid_balance
balance += interest


is equivalent to

balance = unpaid_balance + interest


is equivalent to

balance = unpaid_balance + monthly_rate * unpaid_balance


is equivalent to

balance = unpaid_balance * (monthly_rate + 1)


is equivalent to

balance = (balance - monthly_payment) * (monthly_rate + 1)


so:

def balance_left(balance, monthly_rate, monthly_payment):
monthly_interests_rate = 1 + monthly_rate
for _ in range(MONTHS_IN_A_YEAR):
balance = (balance - monthly_payment) * monthly_interests_rate
return balance


You can also simplify minimal_monthly_payment:

• you can use an infinite loop and return from it instead of using a flag;
• you don't need the test if abs(…) < 0.01 because you know from the previous if and elif that rounded_balance is neither > 0 nor < 0. So rounded_balance is 0 and you have found your minimal monthly payment.
def minimal_monthly_payment(balance, annual_interests):
monthly_rate = annual_interest / MONTHS_IN_A_YEAR
lower_bound = balance / MONTHS_IN_A_YEAR
higher_bound = (monthly_rate + 1)**MONTHS_IN_A_YEAR * lower_bound

while True:
monthly_payment = (higher_bound + lower_bound) / 2
yearly_balance = balance_left(balance, monthly_rate, monthly_payment)
rounded_balance = round(yearly_balance, 2)
if rounded_balance < 0:
higher_bound = monthly_payment
elif rounded_balance > 0:
lower_bound = monthly_payment
else:
return monthly_payment


# Version 2

I find this version more interesting because the algorithm is more efficient. The computation is straightforward and doesn't compute intermediate results that are only a mean to tighten the bounds.

It may benefit from using comments to explain why this approach is computing the right result.

Anyway, we can still improve it: it uses 2 for loops that are almost the same. Can't we make it both be for _ in range(MONTHS_IN_A_YEAR): and merge them? I will necessitate to integrate coefficient = monthly_interests_rate into the loop. Luckyly, (0 + 1) * monthly_interests_rate is monthly_interests_rate:

def annual_rate_coefficient(monthly_interests_rate):
coefficient = 0
for _ in range(MONTHS_IN_A_YEAR):
coefficient = (coefficient + 1) * monthly_interests_rate


so now we can merge both loops:

MONTHS_IN_A_YEAR = 12

def minimal_monthly_payment(balance, annual_interests):
monthly_interests_rate = 1 + annual_interests / MONTHS_IN_A_YEAR
coefficient = 0
for _ in range(MONTHS_IN_A_YEAR):
coefficient = (coefficient + 1) * monthly_interests_rate
balance = balance * monthly_interests_rate
return = balance / coefficient

if __name__ == '__main__':
minimal_cost = minimal_monthly_payment(999999, 0.18)
print('Lowest Payment:', round(minimal_cost, 2))


# Going further

Defining minimal_monthly_payment as a function will allow you to import it in an interactive shell and test it with various amounts and rates. But you could also integrate a command-line parser so you could call your script like:

python <module_name>.py 999999 0.18


You can try to manually take the content from sys.argv and convert it, or let a specialyzed module do the job. For instance argparse:

if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='Minimal monthly payment calculator')
args = parser.parse_args()

minimal_cost = minimal_monthly_payment(args.balance, args.rate)
print('Lowest Payment:', round(minimal_cost, 2))


This is a problem that can be solved exactly with a little math, so there should be no need to program a search.

Suppose that we borrow initial capital $c$ at a monthly interest rate $r$, paying it off in $n$ months with a monthly payment $p$. (Here $r$ is expressed as a multiplier, for example if the monthly percentage rate is $2\%$ then $r = 1.02$.)

In this scenario the outstanding balance is \eqalign{ cr - p &= cr^1 - p(1) & \quad\text{after 1 month;} \\ (cr - p)r - p &= cr^2 - p(1 + r) & \quad\text{after 2 months;} \\ ((cr - p)r - p)r - p &= cr^3 - p(1 + r + r^2) & \quad\text{after 3 months;}} and so after $n$ months the balance is $$cr^n - p(1 + r + r^2 + \dots + r^{n - 1}) = cr^n - p{r^n - 1\over r-1}.$$ If this pays off the debt exactly then $$cr^n - p{r^n - 1\over r-1} = 0$$ and so $$p = {cr^n(r-1) \over r^n - 1}.$$

Since payments typically have to be made in integer numbers of pennies (or whatever the minimum unit of account is) then you'll want to round $p$ up to the nearest penny.

Wrong results

Your implementation says the loan cut (each month) should be 90325, when the correct answer is 91679.90. (See here.)

Use mathematics

Next, let us do some actuarial mathematics. Suppose the initial principal is $A$ (in you program 999999 units), and let $p$ be the monthly payment. Next, let $F_k$ be the balance after $k$ months, let $i$ be the monthly interest rate, and let $m$ be the number of months (in your case $m = 12$). Also, let $r = 1 + i$. Since

$$F_k = rF_{k - 1} - p, \quad k = 1, 2, \dots, m$$

we have the following identities:

\begin{aligned} F_0 &= A, \\ F_1 &= r F_0 - p, \\ F_2 &= r F_1 - p = r^2 F_0 - r p - p, \\ & \dots \\ F_m &= r^m F_0 - \sum_{j = 0}^{m - 1} r^j p \\ &= r^m A - \sum_{j = 0}^{m - 1} r^j p. \end{aligned}

Since we must have paid the loan completely after $m$ months, we must have $F_m = 0$, which yields the following identity:

$$\sum_{j = 0}^{m - 1} r^j p = r^m A,$$

$$p = \frac{r^m A}{\sum_{j = 0}^{m - 1} r^j}$$

Next, let us find the closed form for the sum $S = \sum_{j = 0}^{m - 1} r^j$: \begin{aligned} S - r S &= \sum_{j = 0}^{m - 1} r^j - r \sum_{j = 0}^{m - 1} r^j \\ &= \sum_{j = 0}^{m - 1} r^j - \sum_{j = 1}^m r^j \\ &= 1 - r^m. \end{aligned}

Since $S - r S = S(1 - r) = 1 - r^m$, we deduce that

\begin{aligned} S &= \frac{1 - r^m}{1 - r}. \end{aligned}

Finally, we have \begin{aligned} p &= \frac{r^m A}{\Big( \frac{1 - r^m}{1 - r} \Big)} \\ &= \frac{(1 + i)^m A}{\Big( \frac{1 - (1 + i)^m}{1 - (1 + i)} \Big)} \\ &= \frac{-i (1 + i)^m A}{1 - (1 + i)^m}. \end{aligned}

Compiled into Python, this is

def compute_loan_cut(principal, month_interest_rate, total_months):
divisor = 1 - pow(1 + month_interest_rate, total_months)
return -month_interest_rate * pow(1 + month_interest_rate, total_months)\
* principal / divisor

print("Loan cut:", compute_loan_cut(balance, mRate, 12))


Hope that helps.

• The math would look a lot nicer if you picked a name for the value $1 + i$. Sep 28 '16 at 10:18
• @GarethRees Thank you for your input. I will rewrite the math soon. Sep 28 '16 at 10:25
• @GarethRees Done. Sep 28 '16 at 10:43