show/hide
def fmt_dollar(x, digits=2):
return f"${x:,.{digits}f}"Each topic starts with a real scenario and a bar model, then three levels of exercises. Level 1 (Guided) fills in the blanks of a finished function. Level 2 (Practice) applies it to new numbers. Level 3 (Challenge) asks a new question with little scaffolding. The R and Python exercises run in your browser: click Run Code to try, Submit Answer to check, and use the hint or solution if you get stuck. Excel exercises have a solution you can reveal.
Functions perform operations: calculate a total, build a table, create a graph. The function’s arguments are the inputs it needs to do its job and the output is what it gives back. For example, sum() takes a list of numbers as input and outputs their total.
In R, functions are defined with the function() keyword and assigned to a name. The body goes inside curly braces.
my_fun <- function(x) {
x * 2
}In Python, functions are defined with def, followed by a colon and an indented body. An explicit return statement is required; without it the function returns None.
def my_fun(x):
return x * 2Both languages let you create reusable functions for financial calculations. The key difference is that R automatically returns the last evaluated expression while Python requires return.
Before working through the math, it helps to have two small formatting utilities that handle the $ and % symbols seen throughout the book. They take a plain number and return a reader-friendly string.
In Excel, number formatting is applied through the toolbar. Select the cells to format and choose the appropriate option from the Number Format dropdown.
There are two options for money: Currency and Accounting.1
fmt_dollar() uses an f-string with , for thousands and .{digits}f for decimal places.
def fmt_dollar(x, digits=2):
return f"${x:,.{digits}f}"fmt_pct() takes a decimal rate, multiplies by 100, and formats the result.
def fmt_pct(x, digits=1):
return f"{x * 100:.{digits}f}%"Utility functions like fmt_pct() can be re-used in other, more complex functions.
print(fmt_dollar(9200))
#> $9,200.00
print(fmt_dollar(4200.50))
#> $4,200.50
print(fmt_pct(0.07))
#> 7.0%
print(fmt_pct(0.456))
#> 45.6%fmt_dollar() adds a dollar sign, comma separators, and rounds to a set number of decimal places.
fmt_dollar <- function(x, digits = 2) {
paste0("$", formatC(x, format = "f", digits = digits, big.mark = ","))
}fmt_pct() expects a decimal rate (e.g. 0.07 for 7%) and converts it to a percentage string.
fmt_pct <- function(x, digits = 1) {
paste0(round(x * 100, digits), "%")
}This can be used to format percentages as strings with a percent sign.
fmt_dollar(9200)
#> [1] "$9,200.00"
fmt_dollar(4200.50)
#> [1] "$4,200.50"
fmt_pct(0.07)
#> [1] "7%"
fmt_pct(0.456)
#> [1] "45.6%"Both fmt_dollar() and fmt_pct() follow the same convention: input is always a plain number, output is always a string. They are used throughout the remaining sections to clean up results.
Galloway’s formula becomes real numbers when we picture someone earning $62,400 a year (about the 2025 median for full time workers). They save 15% ($9,360) every year and earn 8%. Each bar below is their savings at a checkpoint, split into what they put in and what it grew into.
Formula: Wealth after n years = Yearly savings × [((1 + r)^n − 1) ÷ r]
Yearly savings = Income × Savings rate, r is the yearly return, and n is the number of years.
The blue layer grows in a straight line, one $9,360 deposit per year. The green layer is growth, and it passes the blue layer after about year 18.
Fill in the blanks to finish wealth_formula(), then find the wealth after 40 years: $62,400 income, 15% saved, 8% return.
Put the inputs in A2 (62400), B2 (0.15), C2 (0.08), and D2 (40). In E2, write a formula for total wealth (money put in plus growth).
=(A2*B2)*(((1+C2)^D2-1)/C2)Or use =FV(C2, D2, -A2*B2). Both return $2,424,769.
Yearly savings is income times the savings rate. The growth factor uses the yearly return.
def wealth_formula(median_income, savings_rate, annual_return, years):
annual_savings = median_income * savings_rate
return annual_savings * (((1 + annual_return) ** years - 1) / annual_return)
wealth_formula(62400, 0.15, 0.08, 40)Yearly savings is income times the savings rate. The growth factor uses the yearly return.
wealth_formula <- function(median_income, savings_rate, annual_return, years) {
annual_savings <- median_income * savings_rate
annual_savings * (((1 + annual_return)^years - 1) / annual_return)
}
wealth_formula(62400, 0.15, 0.08, 40)Same $62,400 income, but now you save 20% for 30 years at 8%. How much of the total is growth (total minus money put in)?
Change B2 to 0.20 and D2 to 30. In F2, find the growth.
=FV(C2, D2, -A2*B2) - A2*B2*D2Growth is $1,039,374 of the $1,413,774 total.
Money put in is yearly savings (income × rate) times the number of years.
total = wealth_formula(62400, 0.20, 0.08, 30)
put_in = 62400 * 0.20 * 30
total - put_inMoney put in is yearly savings (income × rate) times the number of years.
total <- wealth_formula(62400, 0.20, 0.08, 30)
put_in <- 62400 * 0.20 * 30
total - put_inBack to 15% saved at 8%. In which year does growth first become bigger than the money put in?
Put years 1 to 40 in A5:A44. Build columns for total wealth, money put in, and growth, then find the first year where growth is bigger.
In B5, C5, and D5, then fill down to row 44:
=FV($C$2, A5, -$A$2*$B$2)
=$A$2*$B$2*A5
=B5-C5Then find the first year:
=MATCH(TRUE, D5:D44>C5:C44, 0)The answer is year 18.
Use for years in range(1, 41):, compute total and put_in, and break when total - put_in > put_in.
first_year = None
for years in range(1, 41):
total = wealth_formula(62400, 0.15, 0.08, years)
put_in = 62400 * 0.15 * years
if total - put_in > put_in:
first_year = years
break
first_yearwealth_formula() is vectorized, so pass all 40 years at once. Then use min(years[...]) with a comparison inside the brackets.
years <- 1:40
total <- wealth_formula(62400, 0.15, 0.08, years)
put_in <- 62400 * 0.15 * years
min(years[total - put_in > put_in])Fixed costs (rent, loan payments, insurance, utilities) are the bills you can’t easily change month to month. Picture a first job with $3,200 of take-home pay a month and $1,760 of fixed costs. The bar below cuts that paycheck into 10 equal pieces of $320 (each piece is 10%).
Formula: Fixed-Cost Ratio = Fixed Costs ÷ Take-Home Pay × 100
The red section covers 5 and a half of the 10 pieces, so fixed costs take 55% of the paycheck. The ceiling is 60% of $3,200, or $1,920.
Fill in the blanks to finish fixed_cost_ratio(), then find the ratio for $1,760 of fixed costs on $3,200 take-home pay.
Enter fixed costs in A2 (1760) and take-home pay in B2 (3200). In C2, write the ratio as a number out of 100.
=A2/B2*100This returns 55. Or enter =A2/B2 and format the cell as a percentage.
The ratio is the part (fixed costs) divided by the whole (take-home pay).
def fixed_cost_ratio(fixed_costs, take_home):
return (fixed_costs / take_home) * 100
fixed_cost_ratio(1760, 3200)The ratio is the part (fixed costs) divided by the whole (take-home pay).
fixed_cost_ratio <- function(fixed_costs, take_home) {
(fixed_costs / take_home) * 100
}
fixed_cost_ratio(1760, 3200)A job with lower pay brings home $2,800 a month, with the same $1,760 of fixed costs. How much room is left under the 60% ceiling? (A negative answer means you are over.)
Change B2 to 2800. In D2, find the room left under the ceiling.
=B2*0.6-A2This returns -80: the same fixed costs are $80 over the line on a smaller paycheck.
The ceiling is 60% of take-home pay.
take_home = 2800
fixed_costs = 1760
ceiling_60 = take_home * 0.60
ceiling_60 - fixed_costsThe ceiling is 60% of take-home pay.
take_home <- 2800
fixed_costs <- 1760
ceiling_60 <- take_home * 0.60
ceiling_60 - fixed_costsYou bring home $3,200 a month, and your fixed costs other than rent (insurance, phone, loan payment) add up to $560. Write max_fixed_costs(take_home, ceiling) that returns the most you can spend on fixed costs, then use it to find the highest rent that keeps you under 60%.
With take-home pay in B2 (3200), find the highest rent in one formula.
=B2*0.6-560The highest rent is $1,360.
The function only needs one line: take-home pay times the ceiling.
def max_fixed_costs(take_home, ceiling=0.60):
return take_home * ceiling
max_fixed_costs(3200) - 560The function only needs one line: take-home pay times the ceiling.
max_fixed_costs <- function(take_home, ceiling = 0.60) {
take_home * ceiling
}
max_fixed_costs(3200) - 560The pipe operator (|>) passes the output of one function directly into the next function as its first argument, so the code reads left to right instead of inside out. fixed_cost_ratio() already multiplies by 100, so we can pipe its result into round() and then paste0() to add the percent sign.
fixed_cost_ratio(fixed_costs = 1760, take_home = 3200) |> round(1) |> paste0("%")Read that as: compute the ratio, then round it, then add the %. The version without a pipe nests the calls and reads from the inside out, which is harder to follow.
paste0(round(fixed_cost_ratio(fixed_costs = 1760, take_home = 3200), 1), "%")When steps always belong together, wrap them in a single function so callers only see one name.
fixed_cost_pct <- function(fixed_costs, take_home) {
fixed_cost_ratio(fixed_costs, take_home) |>
round(1) |>
paste0("%")
}fixed_cost_pct(fixed_costs = 1760, take_home = 3200)
#> [1] "55%"Python has no native pipe operator. The equivalent is storing the result from fixed_cost_ratio() in a variable and passing it to the next step:
def fixed_cost_pct(fixed_costs, take_home):
ratio = fixed_cost_ratio(fixed_costs, take_home)
return f"{ratio:.1f}%"fixed_cost_pct(fixed_costs=1760, take_home=3200)
#> '55.0%'Python’s approach emphasizes explicit intermediate steps. This reads top to bottom and makes debugging easier since you can inspect ratio along the way.
Read more about the pipe operator in R vs. Python.
Rewrite the nested call below with the pipe so it reads left to right. Use the pricier apartment: $2,160 of fixed costs on $3,200.
Each step after the pipe takes the previous result as its first argument: round(1), then paste0("%").
fixed_cost_ratio(2160, 3200) |> round(1) |> paste0("%")Say a raise adds $400 a month to your take-home pay, and you split it: $200 goes to your lifestyle and $200 gets invested. The chart shows what that single habit produces at 7% over 25 years.
Formula: FV = PMT × [((1 + r)^n − 1) ÷ r]
PMT is the amount invested each month, r is the monthly return (7% ÷ 12), and n is the number of months (25 × 12 = 300).
Fill in the blanks to finish raise_invested_value(), then find the value of investing half of a $400 raise for 25 years at 7%.
Use the built in FV(rate, nper, pmt) function with a monthly rate, a number of months, and the monthly contribution as a negative number.
=FV(7%/12, 25*12, -200)This returns $162,014.34.
The contribution is the share of the raise you invest. The yearly rate is split across 12 months.
def raise_invested_value(monthly_raise, invest_share, rate, years):
contribution = monthly_raise * invest_share
monthly_rate = rate / 12
periods = years * 12
return contribution * (((1 + monthly_rate) ** periods - 1) / monthly_rate)
raise_invested_value(400, 0.5, 0.07, 25)The contribution is the share of the raise you invest. The yearly rate is split across 12 months.
raise_invested_value <- function(monthly_raise, invest_share, rate, years) {
contribution <- monthly_raise * invest_share
monthly_rate <- rate / 12
periods <- years * 12
contribution * (((1 + monthly_rate)^periods - 1) / monthly_rate)
}
raise_invested_value(400, 0.5, 0.07, 25)Now invest 75% of the same $400 raise for 25 years at 7%. How much of the final value is growth?
Find the growth in one formula: the future value minus the money put in.
=FV(7%/12, 25*12, -300) - 300*25*12Growth is $153,022 of the $243,022 total.
75% of $400 is $300 a month, for 25 × 12 months.
value = raise_invested_value(400, 0.75, 0.07, 25)
put_in = 400 * 0.75 * 25 * 12
value - put_in75% of $400 is $300 a month, for 25 × 12 months.
value <- raise_invested_value(400, 0.75, 0.07, 25)
put_in <- 400 * 0.75 * 25 * 12
value - put_inYou invest half of a $400 raise at 7%. How many whole years until it is worth at least $100,000?
Put years 1 to 30 in A2:A31. Find the value at each year, then find the first year at or above $100,000.
In B2, then fill down to row 31:
=FV(7%/12, A2*12, -200)Then:
=MATCH(TRUE, B2:B31>=100000, 0)The answer is 20 years.
Use while raise_invested_value(400, 0.5, 0.07, years) < 100000: and add 1 to years inside the loop.
years = 1
while raise_invested_value(400, 0.5, 0.07, years) < 100000:
years += 1
yearsraise_invested_value() is vectorized, so pass all 30 years at once. Then use min(years[...]).
years <- 1:30
value <- raise_invested_value(400, 0.5, 0.07, years)
min(years[value >= 100000])Read more about the differences between Currency and Accounting formats in Excel.↩︎