A shop owner in a local market wants a business loan of ₹5,00,000 to stock inventory before the festive season, repayable over 24 months. The lender quotes an EMI (Equated Monthly Installment) of ₹23,537 but never states the interest rate plainly, only a line in the fine print about processing charges. A bank down the road is separately offering a working-capital loan at a clearly stated 13% per annum. Which one actually costs less?
Answering that means converting the quoted EMI back into an annual interest rate. The formula connecting principal, tenure, EMI, and rate is standard:
EMI = P × r × (1 + r)^n / ((1 + r)^n − 1)
Here P is the principal, r is the monthly interest rate as a decimal, and n is the number of monthly installments. This formula computes EMI once r is already known. Run it backward, given P, n, and the EMI, wanting r, and ordinary algebra has nothing to offer: the rate sits trapped both outside the power and inside the base (1 + r)^n, and no rearranging pulls it free. This particular equation cannot be solved by algebra at all, a mathematical fact rather than a matter of trying harder, for reasons the next section makes precise. Six lines of Python, by the end of this chapter, will pull the exact rate out of that EMI figure anyway.
Why Some Equations Refuse to Be Solved
Solving ax² + bx + c = 0 for x is routine: the quadratic formula hands you an exact answer built from the coefficients, a square root, and basic arithmetic. Mathematicians in Babylon and India were solving quadratics with equivalent methods thousands of years ago. In the 1500s, Italian mathematicians found similar formulas for cubic equations (degree 3) and quartic equations (degree 4), each messier than the last but still built from nothing more exotic than roots and the four basic operations. It seemed only a matter of time before someone found the formula for degree 5.
Nobody ever found one. This wasn't a case of the search still continuing: in 1824, the Norwegian mathematician Niels Henrik Abel proved that no combination of the usual algebraic operations and roots, applied to the coefficients of a general fifth-degree equation, can produce its solutions. This result is now known as the Abel-Ruffini theorem. Every equation of degree five or higher that does not happen to factor nicely is, from algebra's point of view, simply out of reach.
Now look again at the EMI equation. Multiply both sides by (1 + r)^n − 1 and move every term to one side, and the equation becomes a polynomial in r of degree n + 1. A short four-month loan already produces a degree-5 polynomial, exactly the degree where Abel's theorem shuts the door. The two-year loan above, with n = 24, produces a degree-25 polynomial. There was never going to be an algebra-textbook formula waiting at the end of that calculation.
What replaces algebra in cases like this is a numerical method: a procedure that does not produce an exact symbolic answer but instead generates a sequence of increasingly accurate approximations, each closer to the true answer than the last, and stops once the approximation is close enough to be useful. "Close enough" sounds unrigorous until it is defined precisely, which is exactly what the rest of this chapter does.
The Bisection Method: Halving Your Way to an Answer
Consider a simpler warm-up problem first: find √2, the positive number that squares to 2. Since the Pythagorean era it has been known that √2 is irrational, an infinite, non-repeating decimal, so no finite calculation ever writes it down exactly. Any decimal value anyone has ever produced for it is an approximation obtained by some procedure. Bisection is one of the simplest such procedures.
The idea mirrors a number-guessing game: pick a number between 1 and 100, and every guess gets answered "higher" or "lower," so the best strategy is to guess the midpoint and cut the range in half each time. Bisection applies that same halving strategy to a continuous function instead of a hidden integer. Define f(x) = x² − 2. The number √2 is exactly the positive root of f, the x where f(x) = 0, since f(√2) = (√2)² − 2 = 0. Check f at the endpoints of the interval [1, 2]: f(1) = 1 − 2 = −1 and f(2) = 4 − 2 = 2. One is negative, the other positive, and since f is continuous, it must cross zero somewhere in between. The root is trapped inside [1, 2].
Bisection narrows that trap step by step. At each step, compute the midpoint c of the current interval, evaluate f(c), and check its sign. If f(c) has the opposite sign to f(a), the root lies in the left half [a, c], so c becomes the new right endpoint. Otherwise the root lies in the right half, so c becomes the new left endpoint. Either way, the interval containing the root shrinks to exactly half its previous width. Tracing this by hand for six steps, starting from [1, 2]:
- Step 1: a = 1, b = 2, midpoint c = 1.5, f(c) = 0.25. Positive, so the root is in the left half; new interval [1, 1.5].
- Step 2: a = 1, b = 1.5, midpoint c = 1.25, f(c) = −0.4375. Negative; new interval [1.25, 1.5].
- Step 3: a = 1.25, b = 1.5, midpoint c = 1.375, f(c) = −0.109375. Negative; new interval [1.375, 1.5].
- Step 4: a = 1.375, b = 1.5, midpoint c = 1.4375, f(c) = 0.06640625. Positive; new interval [1.375, 1.4375].
- Step 5: a = 1.375, b = 1.4375, midpoint c = 1.40625, f(c) = −0.0224609375. Negative; new interval [1.40625, 1.4375].
- Step 6: a = 1.40625, b = 1.4375, midpoint c = 1.421875, f(c) = 0.021728515625. Positive; new interval [1.40625, 1.421875].
Each step halves the interval width. After six steps it has shrunk from 1 down to 1/64 = 0.015625, and the true root, 1.41421356…, stays trapped inside [1.40625, 1.421875] throughout, exactly as the sign-change argument guarantees. Handing this same process to Python removes the arithmetic drudgery without changing the idea:
def bisection(f, a, b, tol=1e-6, max_iter=100):
fa = f(a)
if fa * f(b) > 0:
raise ValueError("f(a) and f(b) must have opposite signs")
for i in range(1, max_iter + 1):
c = (a + b) / 2
fc = f(c)
if fc == 0 or (b - a) / 2 < tol:
return c, i
if fa * fc < 0:
b = c
else:
a = c
fa = fc
return (a + b) / 2, max_iter
def f(x):
return x**2 - 2
root, iterations = bisection(f, 1, 2, tol=1e-6)
print(f"Root: {root:.6f}")
print(f"Iterations: {iterations}")
The safety check on the first line, fa * f(b) > 0, rejects any interval that does not bracket a sign change before the loop even starts; without a confirmed sign change, halving proves nothing. Running this prints:
Root: 1.414214
Iterations: 20
Twenty iterations matches the halving logic: the bracket's width is cut in half every step, and since 2^20 is a little over one million, twenty halvings shrink the original width-1 bracket down to roughly one part in a million, right around the resolution tol = 1e-6 demands. Bisection is unglamorous but dependable. Given any interval with a genuine sign change, it always converges, at exactly this pace: one more correct bit of precision per iteration, never faster, never slower.
The Newton-Raphson Method: Following the Tangent
Bisection never uses the shape of f beyond its sign. At every step it looks at the current guess, asks only whether f is positive or negative there, and throws away everything else the function could have told it. The Newton-Raphson method uses more: the slope.
Stand on the curve y = f(x) at a guess, call it x_old, and draw the tangent line there. That tangent has slope f'(x_old), the derivative, and it points roughly in the direction the curve is heading. Follow the tangent line down to where it crosses the x-axis, rather than following the curve itself, and that crossing point is usually a much better guess than x_old was. The tangent line through (x_old, f(x_old)) with slope f'(x_old) is y − f(x_old) = f'(x_old) × (x − x_old). Setting y = 0 and solving for x gives the update rule:
x_new = x_old − f(x_old) / f'(x_old)
For f(x) = x² − 2, the derivative is f'(x) = 2x. Start from x0 = 1.5, the same first guess bisection produced, and trace three updates:
x1 = 1.5 − 0.25 / 3 = 1.41666667, withf(x1) ≈ 6.944 × 10⁻³x2 = 1.41666667 − 0.006944 / 2.833333 = 1.41421569, withf(x2) ≈ 6.007 × 10⁻⁶x3 = 1.41421569 − 0.000006007 / 2.828431 = 1.414213562, withf(x3) ≈ 4.511 × 10⁻¹²
Three updates land within a whisker of √2 = 1.41421356…, while bisection needed twenty steps just to reach six-decimal accuracy. The Python version:
def newton_raphson(f, fprime, x0, tol=1e-9, max_iter=50):
x = x0
for i in range(1, max_iter + 1):
x_new = x - f(x) / fprime(x)
if abs(f(x_new)) < tol:
return x_new, i
x = x_new
return x, max_iter
def f(x):
return x**2 - 2
def fprime(x):
return 2 * x
root, iterations = newton_raphson(f, fprime, 1.5, tol=1e-9)
print(f"Root: {root:.9f}")
print(f"Iterations: {iterations}")
Root: 1.414213562
Iterations: 3
Three iterations, at a tolerance a thousand times stricter than bisection's. That gap is not a coincidence of this particular example. Bisection's error shrinks by a constant factor, roughly half, every step, which is called linear convergence. Newton-Raphson's error roughly squares every step, so the count of correct digits tends to double each time, a property called quadratic convergence. The trade-off is that Newton-Raphson demands more from the problem: it needs f'(x) to exist and be computable, it can divide by something close to zero if the tangent line goes nearly flat, and a poor starting guess can send it diverging away from the root rather than toward it. Bisection asks only for a confirmed sign change and then never fails. Newton-Raphson asks for more information and, when it has it, converges dramatically faster.
Tolerance, Error, and Knowing When to Stop
Both functions above take a tol parameter and stop once some quantity drops below it, but a careful look shows the two loops are not testing the same kind of quantity, and the difference matters.
Start with definitions. If x_true is the exact answer and x_approx is an approximation, the absolute error is |x_true − x_approx|, and the relative error is that same difference divided by |x_true|, usually reported as a percentage. Take bisection's very first midpoint, c = 1.5 from step 1 above: the absolute error is |1.41421356 − 1.5| ≈ 0.0858, and the relative error is 0.0858 / 1.41421356 ≈ 6.07%. Absolute error carries units and can mislead on its own, being off by a metre matters enormously for a room and not at all for a highway, which is why relative error, a unitless proportion, is usually the more meaningful figure when comparing approximations of different sizes. Tolerance is the threshold a programmer picks in advance: stop once the error estimate falls under this number, chosen according to how much precision the task actually needs.
Here is the part that is easy to get wrong. Bisection's loop checks (b - a) / 2 < tol, the bracket's half-width. Because the sign-change invariant guarantees the true root always lies inside [a, b], that half-width is a genuine, provable upper bound on the absolute error of the midpoint: the root cannot be farther from c than half the bracket's width, ever. When bisection's loop stops, the claim "the error is under tol" is backed by a proof, not a hope.
Newton-Raphson's loop checks something else: abs(f(x_new)) < tol. This is the residual, how close the function's output is to zero at the candidate point, and it is not the same quantity as the error, how far the candidate point is from the true root. Newton-Raphson keeps no bracket, so it has no analogous guarantee to fall back on; the residual is simply the cheapest signal available, and a small residual usually, though not provably, means a small error.
The trace above makes the gap between the two concrete. At x3, the residual |f(x3)| is about 4.511 × 10⁻¹², comfortably under tol = 1e-9, which is why the loop returns. The true error at that same point, |x3 − √2|, works out to about 1.595 × 10⁻¹², smaller still, but a different number, not the one the code actually tested. The two stay close because, near a simple root, f looks almost like a straight line, so f(x) ≈ f'(root) × (x − root); here f'(√2) = 2√2 ≈ 2.828, and indeed 4.511 × 10⁻¹² ÷ 1.595 × 10⁻¹² ≈ 2.83. Residual and error are proportional near a well-behaved root, not identical, and the gap between them grows precisely where f' gets small, which is exactly the situation where Newton-Raphson is at its shakiest anyway.
The habit worth keeping from this: before trusting a "converged" message from any numerical routine, check which quantity its stopping test actually measures. A tight tolerance on the wrong quantity is not the same as a tight tolerance on the one that matters.
Bringing It Back: Finding the Real Interest Rate on a Loan
Return to the shop owner's loan: principal P = ₹5,00,000, tenure n = 24 months, quoted EMI of ₹23,537. Rearranged into a polynomial in r, that equation is degree n + 1 = 25, far beyond anything Abel's theorem leaves any hope of solving by radicals. Bisection is not a workaround here; it is the only practical route to the answer.
The same halving idea from the √2 example applies directly, once the unknown is framed the right way. Define g(r) = emi(r, P, n) − 23537, the difference between what a given rate would produce and the EMI actually quoted. The root of g, the rate where g(r) = 0, is exactly the rate baked into the loan. A monthly rate of 0.1% (an annual rate of 1.2%, unrealistically cheap) produces an EMI well under ₹23,537, while a monthly rate of 5% (60% a year, loan-shark territory) produces an EMI well over it. That confirmed sign change brackets the true rate inside [0.001, 0.05], and bisection narrows it down exactly as before.
def emi(r, P, n):
return P * r * (1 + r)**n / ((1 + r)**n - 1)
def solve_rate(P, n, target_emi, lo=0.001, hi=0.05, tol=1e-7, max_iter=100):
def g(r):
return emi(r, P, n) - target_emi
glo = g(lo)
if glo * g(hi) > 0:
raise ValueError("no sign change: widen the bracket")
for i in range(1, max_iter + 1):
mid = (lo + hi) / 2
gmid = g(mid)
if gmid == 0 or (hi - lo) / 2 < tol:
return mid, i
if glo * gmid < 0:
hi = mid
else:
lo = mid
glo = gmid
return (lo + hi) / 2, max_iter
P = 500000 # principal in rupees
n = 24 # tenure in months
quoted_emi = 23537
monthly_rate, iterations = solve_rate(P, n, quoted_emi)
annual_rate = monthly_rate * 12 * 100
print(f"Monthly rate: {monthly_rate:.6f}")
print(f"Annual rate: {annual_rate:.2f}%")
print(f"Iterations: {iterations}")
Monthly rate: 0.010001
Annual rate: 12.00%
Iterations: 19
solve_rate reuses the exact same bisection loop as before, applied to g instead of x² − 2. That is the real power of framing a problem as finding where some function crosses zero: the same code solves it whether the unknown is a square root or an interest rate. The result says the lender's EMI corresponds to a monthly rate of almost exactly 1%, an annual rate of 12%. Against the bank's clearly stated 13% per annum, the shop owner's opaque EMI turns out, once decoded, to be the cheaper loan.
The decision that opened this chapter is now a five-second comparison instead of a guess: 12% from the inventory lender against a stated 13% from the bank, so the first loan wins, fine print and all. That is what numerical methods are for. Not every equation worth solving has a formula waiting at the end of the algebra, and for most of the equations that show up in real financial contracts, engineering constraints, and scientific models, no such formula exists or ever will. What does exist, always, is a way to start with a guess, measure how wrong it is, and use that measurement to produce a better guess, again and again, until the error is smaller than anyone practically cares about. Bisection and Newton-Raphson are two ways of doing that. A third, gradient descent, drives the training of virtually every neural network in use today, nudging a model's parameters step by step to shrink a loss function toward zero: the same idea from this chapter, aimed at a different function.
Think About It
Think about this: How would you explain numerical methods and python implementation to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where numerical methods and python implementation is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting numerical methods and python implementation to at least 3 other topics you have studied.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind numerical methods and python implementation, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.