Income

TipHow these exercises work

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.

1 Book Topics

  • Take-home pay: your salary (gross pay) is not what lands in your account; taxes, Social Security and Medicare (7.65%), 401(k), and health plans come out first, so plan around take-home pay
  • Wealth vs. spending: true wealth is the income you didn’t spend, quietly compounding (Housel); conspicuous consumption is negatively correlated with actual wealth (Felix)
  • Human capital: early in a career, future earning potential is the biggest asset; invest in skills and career growth, and take more stock market risk when young (Felix)
  • Spending and happiness: experiences beat possessions; buying time is a high return use of money; lifestyle inflation is the silent killer; housing is the biggest lever
  • Three anchoring rules: spend on what you love, cut what you don’t (Sethi); build a gap between ego and income (Housel); keep it simple (Bogle)
  • Wealth formula: Wealth = Focus + (Stoicism × Time × Diversification); focus is additive, the other three multiply, so a zero on any one collapses the second term (Galloway)
  • As income grows: split every raise, 50% to investing and 50% to lifestyle
  • Math: the wealth formula as yearly savings compounding ($62,400 income, 15% saved, 8% for 40 years), the fixed-cost ratio ($1,760 of $3,200 take-home pay, under a 60% ceiling), and what a raise is really worth ($200/month at 7% for 25 years)

2 Writing Functions

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 * 2

Both 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.

3 Number Formatting

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.

Common number formats in Excel

Common number formats in Excel

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.

show/hide
def fmt_dollar(x, digits=2):
    return f"${x:,.{digits}f}"

fmt_pct() takes a decimal rate, multiplies by 100, and formats the result.

show/hide
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.

show/hide
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.

show/hide
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.

show/hide
fmt_pct <- function(x, digits = 1) {
  paste0(round(x * 100, digits), "%")
}

This can be used to format percentages as strings with a percent sign.

show/hide
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.

4 The Wealth Formula

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.

$9,360 saved every year at 8%: money put in vs. growth

$9,360 saved every year at 8%: money put in vs. growth

$9,360 saved every year at 8%: money put in vs. growth

$9,360 saved every year at 8%: money put in vs. growth

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.

4.1 Level 1: Guided

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)

4.2 Level 2: Practice

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*D2

Growth 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_in

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_in

4.3 Level 3: Challenge

Back 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-C5

Then 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_year

wealth_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])

5 Fixed-Cost Ratio

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

$3,200 take-home pay in 10 units of $320: fixed costs vs. flexible spending

$3,200 take-home pay in 10 units of $320: fixed costs vs. flexible spending

$3,200 take-home pay in 10 units of $320: fixed costs vs. flexible spending

$3,200 take-home pay in 10 units of $320: fixed costs vs. flexible spending

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.

5.1 Level 1: Guided

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*100

This 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)

5.2 Level 2: Practice

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-A2

This 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_costs

The ceiling is 60% of take-home pay.

take_home <- 2800
fixed_costs <- 1760
ceiling_60 <- take_home * 0.60
ceiling_60 - fixed_costs

5.3 Level 3: Challenge

You 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-560

The 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) - 560

The 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) - 560

6 Using Pipes in R

The 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.

6.1 Exercise: Pipes

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("%")

7 What a Raise Is Really Worth

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).

$200/month invested at 7%: value at each 5-year checkpoint

$200/month invested at 7%: value at each 5-year checkpoint

$200/month invested at 7%: value at each 5-year checkpoint

$200/month invested at 7%: value at each 5-year checkpoint

7.1 Level 1: Guided

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)

7.2 Level 2: Practice

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*12

Growth 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_in

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_in

7.3 Level 3: Challenge

You 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

years

raise_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])

Footnotes

  1. Read more about the differences between Currency and Accounting formats in Excel.↩︎