Math laboratory
Percentage changes multiply, they do not add
Computes exact multipliers for successive percentage changes with fractions: up p% then down p% gives 1 - (p/100)^2; adding two percentages gives the right answer only when one of them is 0 (searched for all whole p, q from -99 to 100); a rise of p% is undone by a fall of 100p/(100 + p)%; five 10% rises give 61.051%, not 50%. Exact, finite checks of identities the lesson proves in general.
"""Percentage changes multiply: exact arithmetic with fractions.
A rise of p% multiplies an amount by 1 + p/100 and a fall of p% by
1 - p/100. So "up p%, then down p%" multiplies by (1 + p/100)(1 - p/100),
which is 1 - (p/100)**2: always a little less than 1. This experiment
computes these factors exactly with Python's Fraction type, searches for the
pairs of changes where simply adding the percentages happens to work, finds
the fall that undoes a rise (100p/(100 + p) percent), and compares five 10%
rises with one 50% rise.
"""
from fractions import Fraction
def factor(p):
"""The multiplier for a change of p percent (p may be negative)."""
return 1 + Fraction(p) / 100
def combined(*changes):
"""The single multiplier equivalent to several percentage changes."""
total = Fraction(1)
for p in changes:
total *= factor(p)
return total
def as_percent_change(multiplier):
"""Turn a multiplier such as 99/100 into a percentage change (-1)."""
return (Fraction(multiplier) - 1) * 100
def undo_rise(p):
"""The percentage fall that exactly undoes a rise of p percent."""
p = Fraction(p)
if p <= -100:
raise ValueError("a fall of 100% or more cannot be undone")
return 100 * p / (100 + p)
def when_adding_works(low, high):
"""Whole-number pairs (p, q) in range where adding the percentages works.
Adding guesses the multiplier 1 + (p + q)/100; the true multiplier is
(1 + p/100)(1 + q/100) = 1 + (p + q)/100 + pq/10000. Returns the list of
pairs where the guess is exactly right, and the number of pairs tried.
"""
hits = []
tried = 0
for p in range(low, high + 1):
for q in range(low, high + 1):
tried += 1
if combined(p, q) == factor(p + q):
hits.append((p, q))
return hits, tried
def fmt(x):
"""Show a Fraction as a decimal when it is one, else as a fraction."""
x = Fraction(x)
if x.denominator == 1:
return str(x.numerator)
if all(p in (2, 5) for p in _prime_factors(x.denominator)):
return f"{float(x):.6f}".rstrip("0")
return f"{x} (about {float(x):.2f})"
def _prime_factors(n):
"""Prime factors of n > 0 (with repetition)."""
out, d = [], 2
while d * d <= n:
while n % d == 0:
out.append(d)
n //= d
d += 1
if n > 1:
out.append(n)
return out
def main():
print("Up p% then down p%: multiplier (1 + p/100)(1 - p/100)")
print(" p multiplier overall change")
for p in [5, 10, 20, 25, 50, 100]:
m = combined(p, -p)
print(f" {p:3d}% {fmt(m):>10} {fmt(as_percent_change(m)):>8}%")
hits, tried = when_adding_works(-99, 100)
zero = all(p == 0 or q == 0 for p, q in hits)
print(f"Adding p% and q% gives the right answer for {len(hits)} of "
f"{tried} pairs")
print(f" (whole p, q in -99..100); every one has p = 0 or q = 0: {zero}")
print()
print("The fall that undoes a rise of p%:")
for p in [10, 25, 50, 100, 300]:
print(f" rise {p:3d}% -> fall {fmt(undo_rise(p))}%")
print()
five = combined(10, 10, 10, 10, 10)
print(f"Five rises of 10%: multiplier {fmt(five)}, "
f"a rise of {fmt(as_percent_change(five))}%, not 50%.")
print()
print("Epistemic status: exact rational arithmetic. The search is")
print("exhaustive for 40,000 whole-number pairs only; that adding works")
print("exactly when pq = 0, for every p and q, follows from the algebra")
print("(1 + p/100)(1 + q/100) = 1 + (p + q)/100 + pq/10000 in the lesson.")
if __name__ == "__main__":
main()Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.