Budgeting

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

  • Mindset shift: a budget is intentional allocation, not restriction; reframe “I have to spend” as “my needs cost”; room for error is the most underrated concept (Housel)
  • Expense categories: fixed (lock in and automate), variable (category limits, not line by line tracking), periodic (set aside 1/12 of the annual cost each month)
  • Sinking funds: $1,200/year car insurance becomes $100/month; gifts $600 → $50, car maintenance $1,200 → $100, vacation $2,400 → $200
  • Zero-based budgeting: every dollar of a $5,000 paycheck gets a job and the remainder is $0; best for anyone rebuilding from debt or with irregular income
  • Percentage-based budgeting: Sethi’s Conscious Spending Plan (fixed 50 to 60%, investments 10%+, savings 5 to 10%, guilt-free 20 to 35%) next to the 50/30/20 rule
  • Recommendation: zero-based for 2 to 3 months, then graduate to percentages with automation
  • Tools: a spreadsheet with tabs for income, fixed, variable, sinking funds, net worth, and a summary; the best tool is the one you will actually use
  • Budgeting math: percentages, annualizing and monthly-izing, opportunity cost, break-even
  • Financial Order of Operations: the budget shows how much is left over; Preston’s FOO shows where it goes next

2 Percentages

The foundational budgeting calculation: what share of income does each expense represent? Picture income as one bar and each expense as a slice of it.

Formula: (Expense ÷ Income) × 100

Book example: rent of $1,500 on $5,000 of take-home pay is 30% of income.

Rent ($1,500) as a share of a $5,000 paycheck

Rent ($1,500) as a share of a $5,000 paycheck

Rent ($1,500) as a share of a $5,000 paycheck

Rent ($1,500) as a share of a $5,000 paycheck

2.1 Level 1: Guided

Fill in the blanks to finish percent_of_income(), then find what share of a $5,000 paycheck goes to $1,500 of rent.

With the expense in B2 (1500) and income in C2 (5000), write the percentage in D2.

=(B2 / C2) * 100

This returns 30. Or enter =B2 / C2 and format the cell as a Percentage.

The share is the part (the expense) divided by the whole (income).

def percent_of_income(expense, income):
    return (expense / income) * 100

percent_of_income(1500, 5000)

The share is the part (the expense) divided by the whole (income).

percent_of_income <- function(expense, income) {
  (expense / income) * 100
}
percent_of_income(1500, 5000)

2.2 Level 2: Practice

Here is the book’s zero-based budget for a $5,000 paycheck. What percent of income goes to Investments and Savings combined?

Category Amount
Rent $1,500
Groceries $500
Transport $300
Investments $1,000
Savings $500
Fun Money $700
Other $500

Put categories in A2:A8, amounts in B2:B8, and income in E1. Find the combined percentage for Investments and Savings.

In C2, lock the income cell with $ and fill down to C8:

=B2 / $E$1 * 100

Then add the two rows: =C5 + C6. The answer is 30.

isin() takes a list: ["Investments", "Savings"].

budget["pct"] = percent_of_income(budget["amount"], 5000)
budget.loc[budget["category"].isin(["Investments", "Savings"]), "pct"].sum()

%in% takes a vector: c("Investments", "Savings").

budget |>
  mutate(pct = percent_of_income(amount, 5000)) |>
  filter(category %in% c("Investments", "Savings")) |>
  summarise(total = sum(pct)) |>
  pull(total)

2.3 Level 3: Challenge

Rent goes up from $1,500 to $1,800. To keep the budget zero-based (every dollar still adds up to $5,000), you take the extra $300 out of Fun Money. What percent of income is Fun Money now?

Change the rent amount in B2, then change B7 (Fun Money) so B9 (the total) is still 5,000.

Set B2 to 1800 and B7 to =700 - (B2 - 1500), which is 400. C7 then shows 8, and C9 still reads 100.

Change a single cell with budget.loc[budget["category"] == "Rent", "amount"] = 1800. Check that budget["amount"].sum() is still 5000.

budget.loc[budget["category"] == "Rent", "amount"] = 1800
budget.loc[budget["category"] == "Fun Money", "amount"] -= 300
fun = budget.loc[budget["category"] == "Fun Money", "amount"].sum()
percent_of_income(fun, 5000)

Inside mutate(), use case_when() to change only the Rent and Fun Money rows, and .default = amount to keep the rest. Check that sum(amount) is still 5000.

budget |>
  mutate(amount = case_when(
    category == "Rent" ~ 1800,
    category == "Fun Money" ~ amount - 300,
    .default = amount
  )) |>
  filter(category == "Fun Money") |>
  mutate(pct = percent_of_income(amount, 5000)) |>
  pull(pct)

3 Annualizing and Monthly-izing

Scale any spending habit across time periods to see the true cost. Each period is the shorter one multiplied by how many times it fits.

Formula: Cost over a longer period = Cost per period × Number of periods it contains

Daily → Weekly: × 7

Weekly → Monthly: × 4.33 (not 4)

Monthly → Annual: × 12

Daily → Annual: × 365

Annual → Monthly: ÷ 12

Book example: a $5 daily coffee is $5 × 365 = $1,825 per year.

The same $5/day habit across four time periods

The same $5/day habit across four time periods

The same $5/day habit across four time periods

The same $5/day habit across four time periods

3.1 Level 1: Guided

Fill in the blanks to finish to_annual(), then find the yearly cost of a $5 daily coffee.

Put the daily cost in A1 (5). In A2, find the yearly cost.

=A1 * 365

This returns $1,825.

How many days are in a year? How many months?

def to_annual(amount, period="daily"):
    factors = {"daily": 365, "weekly": 52, "monthly": 12}
    return amount * factors[period]

to_annual(5, "daily")

How many days are in a year? How many months?

to_annual <- function(amount, period = "daily") {
  factors <- c(daily = 365, weekly = 52, monthly = 12)
  amount * factors[[period]]
}
to_annual(5, "daily")

3.2 Level 2: Practice

The book’s sinking funds turn yearly costs into monthly set-asides: car insurance $1,200, gifts $600, car maintenance $1,200, and a vacation $2,400. How much do you need to set aside each month for all four?

Put the names in A2:A5 and yearly costs in B2:B5. Find each monthly amount in column C and the total in C6.

In C2, then fill down:

=B2 / 12

Then =SUM(C2:C5) returns $450.

Divide the whole annual column by 12.

sinking["monthly"] = sinking["annual"] / 12
sinking["monthly"].sum()

Divide the annual column by 12.

sinking |>
  mutate(monthly = annual / 12) |>
  summarise(total = sum(monthly)) |>
  pull(total)

3.3 Level 3: Challenge

Three habits are paid on different schedules: coffee $5 a day, groceries $150 a week, and streaming $15.99 a month. What do they cost together in a year?

Put habits in A2:A4, amounts in B2:B4, and periods (“daily”, “weekly”, “monthly”) in C2:C4. Find each yearly cost in D and the total.

In D2, then fill down:

=B2 * SWITCH(C2, "daily", 365, "weekly", 52, "monthly", 12)

Then =SUM(D2:D4) returns $9,816.88.

to_annual() takes one row at a time. Use habits.apply(lambda row: to_annual(row["amount"], row["period"]), axis=1).

habits["annual"] = habits.apply(lambda row: to_annual(row["amount"], row["period"]), axis=1)
habits["annual"].sum()

to_annual() takes one period at a time. Inside mutate(), use mapply(to_annual, amount, period) to run it on each row.

habits |>
  mutate(annual = mapply(to_annual, amount, period)) |>
  summarise(total = sum(annual)) |>
  pull(total)

4 Opportunity Cost

Every dollar spent is also what that dollar could have become. The hidden cost of any purchase is its future value if invested instead.

Formula: amount × (1 + r)^n

Book example: choosing a $15,000 used car over a $40,000 new one and investing the $25,000 difference at 7% for 30 years gives $25,000 × (1.07)^30 ≈ $190,000.

$25,000 invested at 7%: value at each checkpoint

$25,000 invested at 7%: value at each checkpoint

$25,000 invested at 7%: value at each checkpoint

$25,000 invested at 7%: value at each checkpoint

4.1 Level 1: Guided

Fill in the blanks to finish opportunity_cost(), then find what the $25,000 saved on a used car grows to in 30 years at 7%.

Put the amount in A2 (25000), the rate in B2 (0.07), and years in C2 (30). Find the future value in D2.

=A2*(1+B2)^C2

This returns $190,306.38.

Grow by (1 + rate) once for every year.

def opportunity_cost(amount, rate, years):
    return amount * (1 + rate) ** years

opportunity_cost(25000, 0.07, 30)

Grow by (1 + rate) once for every year.

opportunity_cost <- function(amount, rate, years) {
  amount * (1 + rate)^years
}
opportunity_cost(25000, 0.07, 30)

4.2 Level 2: Practice

Returns are never guaranteed. Using a cautious 5% return instead, what does the same $25,000 grow to in 30 years?

Change B2 to 0.05.

The same =A2*(1+B2)^C2 now returns $108,048.56. Even the cautious rate makes the used car worth more than $100,000.

Pass the amount, the rate as a decimal (0.05), and the years.

opportunity_cost(25000, 0.05, 30)

Pass the amount, the rate as a decimal (0.05), and the years.

opportunity_cost(25000, 0.05, 30)

4.3 Level 3: Challenge

At 7%, how many whole years does it take for the $25,000 to double to $50,000?

Put years 1 to 20 in A5:A24, find the value at each year in B, then find the first year at or above $50,000.

In B5, then fill down:

=$A$2*(1+$B$2)^A5

Then =MATCH(TRUE, B5:B24>=50000, 0) returns 11. You can also solve it directly with =ROUNDUP(NPER(7%, 0, -25000, 50000), 0).

Use while opportunity_cost(25000, 0.07, years) < 50000: and add 1 to years inside the loop.

years = 1
while opportunity_cost(25000, 0.07, years) < 50000:
    years += 1

years

opportunity_cost() is vectorized, so pass all 20 years at once. Then use min(years[...]).

years <- 1:20
value <- opportunity_cost(25000, 0.07, years)
min(years[value >= 50000])

5 Break-Even

Useful for evaluating memberships, subscriptions, refinances, or any purchase with an upfront cost and recurring savings. Break-even is the first month cumulative savings meets the upfront cost.

Formula: Upfront Cost ÷ Monthly Savings = months to break even

Book example: a $500 membership that saves $60/month breaks even in $500 ÷ $60 = 8.3 months.

$60/month savings accumulating toward a $500 upfront cost

$60/month savings accumulating toward a $500 upfront cost

$60/month savings accumulating toward a $500 upfront cost

$60/month savings accumulating toward a $500 upfront cost

5.1 Level 1: Guided

Fill in the blanks to finish months_to_break_even(), then find how long a $500 membership that saves $60 a month takes to pay for itself.

With the upfront cost in A2 (500) and monthly savings in B2 (60), find the months in C2.

=A2 / B2

This returns 8.33 months.

How many months of savings does it take to cover the upfront cost?

def months_to_break_even(upfront_cost, monthly_savings):
    return upfront_cost / monthly_savings

months_to_break_even(500, 60)

How many months of savings does it take to cover the upfront cost?

months_to_break_even <- function(upfront_cost, monthly_savings) {
  upfront_cost / monthly_savings
}
months_to_break_even(500, 60)

5.2 Level 2: Practice

Refinancing a loan costs $3,000 upfront and lowers your payment by $110 a month. Savings arrive in whole months, so in which month do you break even?

Put 3000 in A2 and 110 in B2. Round up to a whole month.

=ROUNDUP(A2 / B2, 0)

This returns 28. If you plan to sell the house sooner than that, the refinance doesn’t pay.

27.3 months means you cross the line in month 28. Which math function always rounds up?

math.ceil(months_to_break_even(3000, 110))

27.3 months means you cross the line in month 28. Which function always rounds up?

ceiling(months_to_break_even(3000, 110))

5.3 Level 3: Challenge

A purchase that saves nothing (or costs more each month) never breaks even, but plain division gives a misleading answer. Write safe_break_even(upfront_cost, monthly_savings) that returns infinity when savings are zero or less, and the usual months otherwise. Test it on savings of $0, -$10, and $60.

Write a formula in C2 that shows “Never” when B2 is zero or less, and the whole months otherwise.

=IF(B2 <= 0, "Never", ROUNDUP(A2 / B2, 0))

Check if monthly_savings <= 0: first and return math.inf. Otherwise return the division.

import math
def safe_break_even(upfront_cost, monthly_savings):
    if monthly_savings <= 0:
        return math.inf
    return upfront_cost / monthly_savings

[safe_break_even(500, 0), safe_break_even(500, -10), safe_break_even(500, 60)]

Check if (monthly_savings <= 0) first and return(Inf). Otherwise return the division.

safe_break_even <- function(upfront_cost, monthly_savings) {
  if (monthly_savings <= 0) {
    return(Inf)
  }
  upfront_cost / monthly_savings
}
c(safe_break_even(500, 0), safe_break_even(500, -10), safe_break_even(500, 60))