General comments
- 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')
parser.add_argument('balance', type=int, help='initial lending')
parser.add_argument('rate', type=float, help='annual interests rate')
args = parser.parse_args()
minimal_cost = minimal_monthly_payment(args.balance, args.rate)
print('Lowest Payment:', round(minimal_cost, 2))