Build a Route-Decision Model

Score three real estate development paths on the same four axes.

Introduction

30 Second Summary

Choosing how to enter real estate development is one of the most expensive decisions you can make. Most people decide based on gut feeling, ignoring that a "free" funded PhD still costs five years of developer salary never earned.

In this project, you will build a Python model that scores three entry routes on the same four axes using published program costs and BLS wage data. The model outputs a one-page decision brief with a recommendation, break-even years, and the single input change that flips the answer.

What You'll Build

Run a single command and the model prints a break-even table showing when each degree route pays back its cost, names the recommended route at your time horizon, and tells you the one input change that would reverse the answer.

By the end of this project, you'll have:

  • A decision model you run from the terminal to score the PhD, MRED, and Experience+CCIM routes on the same four axes (total cost, forgone earnings, time to competency, expected outcome), printing a comparison table sourced from published BLS and program data.
  • A break-even calculator that tells you the exact year each degree route's earnings advantage repays its upfront cost, using discounted NPV from numpy-financial.
  • A one-page decision brief in markdown you can hand to a mentor, naming your recommendation, the break-even table, and the specific input change that flips the answer.
  • Secret Mission: A teach-back instrument that scores whether you can defend the recommendation and name what would reverse it.

Are there any prerequisites?

You need basic Python knowledge (variables, functions, dictionaries, f-strings) and an active subscription to an AI coding client like Cursor, Claude Code, Codex via ChatGPT, or Gemini. You also need a GitHub account and a Linear account. No financial modeling experience is required.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters that the answer comes from a model rather than an opinion.

Preflight and Setup

The model cannot compute anything without verified inputs, a running Python environment, and confirmed tooling.

In this step, you will confirm your AI client subscription, stand up the project infrastructure (a private repo and a ticket board), then write the first function that loads published route data into a table.

In this step, get ready to:
  • Confirm your AI client and create the GitHub repo, Linear project, and decision record.
  • Install Python 3.11+ and the project dependencies inside a virtual environment.
  • Build the data-loading agent and verify it prints the three-route input table.
Confirm your AI client and create project infrastructure

Before writing any code, you need three things in place: a confirmed AI coding assistant, a version-controlled home for the model, and a ticket board to track each build step.

  • Open your AI coding client (Cursor, Claude Code, Codex via ChatGPT, or Gemini via Google Antigravity) and confirm your subscription is active and within its usage limits for this session.
  • Navigate to github.com/new in your browser.
  • Enter first-choice as the repository name.
  • Select Private under the visibility section.
  • Click Create repository.

Why a private repo?

The model contains your personal decision data and published figures used under fair-use commentary. A private repo keeps it out of public search while you iterate.

  • Open Linear and create a new project called First Choice.
  • Create one ticket for each build step: Preflight and Setup, Four-Axis Calculation, Break-Even and Naive Mistake, Sensitivity and Flip, Topology and Documentation.

Linear ticket workflow

Each ticket gets its own branch. When you push the branch and open a pull request, merging the PR closes the ticket. This gives you a clean audit trail for every modeling decision.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the repo by running this command (replace the URL with your own repo URL from GitHub):
git clone [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/first-choice.git"]]
  • Move into the project folder by running:
cd first-choice
  • Confirm the clone worked by running:
ls -a

You should see a .git folder in the output. That confirms this directory is a Git repository.

  • Create your first branch for this step by running:
git checkout -b preflight-setup
  • Create DECISIONS.md in your project folder with the following content:
# Decision Record

## Decision 1
Use Python with pandas and numpy-financial over a spreadsheet because the model must be parameterized, reproducible, and agent-patterned. Three separate spreadsheets could not be compared on the same axes in one run.

What is a decision record?

A decision record captures each modeling choice and its reasoning. When you revisit the model months later (or hand it to a mentor), the record explains why you chose Python over a spreadsheet, why certain wage proxies were selected, and what assumptions were made.

  • Save DECISIONS.md.
  • Confirm it exists by running:
ls

You should see DECISIONS.md listed in the output.

Don't see DECISIONS.md?

Make sure you saved the file inside the first-choice folder, not one level up on your Desktop. Run pwd to confirm your terminal is in the right directory.

Help me find where my file was saved.

Install Python and dependencies

The model uses pandas for tabular data, NumPy for numeric operations, and numpy-financial for net present value calculations. All three require Python 3.11+ to run.

  • Check your Python version by running:
python3 --version

✔️ I see version 3.11 or higher

Your Python version is ready. Move on to creating the virtual environment below.

ⓧ I see an older version

Your system has an older Python that will not work with pandas==2.2.3. You need Python 3.11 or higher.

On macOS, the system Python is often outdated. Install the correct version using a version manager or the official installer.

  • Download Python 3.11+ from python.org/downloads and run the installer.
  • After installing, run python3.11 --version to confirm the new version is available.
  • Use python3.11 instead of python3 for the rest of this project if your default still points to the old version.

Use pyenv for clean version management

If you want to manage multiple Python versions cleanly, install pyenv and run pyenv install 3.11 followed by pyenv local 3.11 inside your project folder. This pins the version without affecting the rest of your system.

ⓧ Command not found

Python is not installed on your machine yet.

  • Download Python 3.11+ from python.org/downloads and run the installer.
  • On Windows, check the box labeled Add Python to PATH during installation.
  • Close and reopen your terminal after installing.
  • Run python3 --version again to confirm you see 3.11 or higher.
  • Create a virtual environment by running:
python3 -m venv .venv
  • Activate the virtual environment by running:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

Your terminal prompt now shows (.venv) at the beginning, confirming the virtual environment is active.

  • Install the project dependencies by running:
pip install pandas==2.2.3 numpy==2.5.1 numpy-financial==1.0.0

You should see Successfully installed followed by the three package names and their versions.

Seeing a permissions error or resolution failure?

Make sure your terminal shows (.venv) in the prompt. If it does not, the virtual environment is not active and pip is trying to install globally.

If you see a version conflict, confirm you are running Python 3.11+ inside the venv by running python --version.

Help me fix my pip install error.

Create the data-loading agent

The first agent in the model is a data loader. It reads published program costs and BLS wage figures into a structured table so every downstream calculation starts from the same sourced inputs.

  • Create a new file called config.py in your first-choice folder with the following content:
"""Baseline assumptions for the route-decision model."""

# Discount rate for NPV calculations
DISCOUNT_RATE = 0.05

# The wage the learner forgoes while in a program.
# Proxy: BLS Construction Managers (SOC 11-9021) median, May 2024.
BASELINE_WAGE = 106_980

# Time horizon for the recommendation (years after entering the field)
TIME_HORIZON_YEARS = 10

What do these constants control?

DISCOUNT_RATE determines how much future earnings are worth today. A 5% rate means $1 earned 10 years from now is worth about 61 cents in today's dollars.

BASELINE_WAGE is the salary the learner gives up while in a program. It comes from the BLS median for Construction Managers (SOC 11-9021), the closest proxy for someone already working in development-adjacent roles.

TIME_HORIZON_YEARS sets how far out the model looks when deciding which route pays back first.

  • Save config.py.
  • Confirm it exists by running:
ls

You should now see both config.py and DECISIONS.md listed.

  • Create a new file called route_model.py and add the imports and the load_routes() function:
"""The First Choice: Route-Decision Model for Real Estate Development."""

import sys
import pandas as pd
import numpy as np
import numpy_financial as npf

from config import BASELINE_WAGE, DISCOUNT_RATE, TIME_HORIZON_YEARS


def load_routes() -> pd.DataFrame:
    """Load the three routes with published inputs and sources."""
    data = {
        "route": ["PhD", "MRED", "Experience+CCIM"],
        "program_cost": [98_662, 98_662, 8_286],
        "duration_years": [5, 1.5, 4],
        "annual_stipend": [0, 0, 0],
        "post_program_ramp_years": [2, 0, 0],
        "outcome_annual_wage": [119_980, 106_980, 106_980],
        "source": [
            "PhD unfunded at MRED-equivalent tuition; BLS SOC 25-1063 Economics Teachers $119,980 (May 2024)",
            "MIT MSRED: $98,662 (2026-27); BLS SOC 11-9021 Construction Managers $106,980 (May 2024)",
            "CCIM Institute: $8,286 member path (2026); BLS SOC 11-9021 Construction Managers $106,980 (May 2024)",
        ],
    }
    return pd.DataFrame(data)

What does this data represent?

Each row is one entry route into real estate development. The PhD route leads to research and teaching. The MRED teaches the development craft directly. The Experience+CCIM route is how most developers actually learn, through deals plus the Certified Commercial Investment Member designation.

Every value carries its source so the model is auditable. The program_cost comes from published program pages. The outcome_annual_wage comes from the BLS Occupational Employment and Wage Statistics, using named SOC proxy codes because no exact "Real Estate Developer" occupation exists.

  • Below the load_routes() function, add the main() function and entry point:

def main():
    routes = load_routes()
    print("--- Input Table ---")
    print(routes[["route", "program_cost", "duration_years", "annual_stipend", "outcome_annual_wage"]].to_string(index=False))
    print()


if __name__ == "__main__":
    main()

What does main() do?

The main() function calls load_routes() and prints a clean table showing the four numeric columns for each route. The if __name__ == "__main__" guard ensures this only runs when you execute the file directly, not when other modules import from it.

  • Save route_model.py.

Before you run the model, what do you expect to see? Three rows of data (one per route) with numeric columns showing each route's cost, duration, stipend, and outcome wage.

  • Run the model by executing:
python route_model.py

You should see the input table printed with three rows (PhD, MRED, Experience+CCIM) and columns for program cost, duration, stipend, and outcome wage.

Seeing a ModuleNotFoundError?

If Python cannot find pandas or numpy_financial, your virtual environment is not active. Check that your terminal prompt shows (.venv). If it does not, re-activate it and try again.

If you see No module named 'config', make sure config.py is in the same folder as route_model.py. Run ls to confirm both files are side by side.

Help me fix my import error when running route_model.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both config.py and route_model.py.

ⓧ I'd like to double check the full code

Scroll up and confirm you have two chunks in route_model.py:

  • The imports and load_routes() function (the first code block you added).
  • The main() function and if __name__ entry point (the second code block).

And config.py should contain three constants: DISCOUNT_RATE, BASELINE_WAGE, and TIME_HORIZON_YEARS.

  • Stage and commit all files by running:
git add .
git commit -m "Add data-loading agent with three-route inputs"
  • Push the branch and open a pull request on GitHub by running:
git push -u origin preflight-setup
  • Open the pull request on GitHub and merge it to close the Preflight and Setup ticket in Linear.

Your environment is ready and the data-loading agent works. Next up, you will score all three routes on the same four axes so they can be compared with arithmetic instead of opinions.

Compute the Four Axes

Your input table has raw numbers: tuition, duration, stipend, wage. But raw inputs are not comparable. Saying the PhD costs $99k and the CCIM costs $8k tells you nothing until you also count the salary you gave up while enrolled.

This step applies the same-axes scoring pattern: every route gets scored on identical axes using the same formula. Forgone earnings sit next to tuition on every route. This is what stops a funded PhD from looking free.

In this step, get ready to:
  • Add a compute_axes() function that scores all three routes on total cost, forgone earnings, time to competency, and expected outcome.
  • Wire the function into main() and observe which route wins on cost vs. which wins on time.
  • Record the wage-proxy decision and close the Linear ticket.
Add the compute_axes() function

Each route needs to be scored on four identical measures so the comparison is fair.

  • Total cost: program tuition minus any stipend received during the program.
  • Forgone earnings: the salary you would have earned during those years, minus any stipend.
  • Time to competency: program duration plus any post-program ramp (the PhD job market takes ~2 years).
  • Expected outcome: the BLS median wage for the role the route leads to.
  • In route_model.py, add the following function below load_routes():
def compute_axes(routes: pd.DataFrame, baseline_wage: float) -> pd.DataFrame:
    """Compute the four axes for each route."""
    axes = pd.DataFrame()
    axes["route"] = routes["route"]
    axes["total_cost"] = (
        routes["program_cost"]
        - routes["annual_stipend"] * routes["duration_years"]
    )
    axes["forgone_earnings"] = (
        baseline_wage * routes["duration_years"]
        - routes["annual_stipend"] * routes["duration_years"]
    )
    axes["time_to_competency"] = (
        routes["duration_years"] + routes["post_program_ramp_years"]
    )
    axes["expected_outcome"] = routes["outcome_annual_wage"]
    return axes

What does this code do?

  • total_cost subtracts any stipend from the program's sticker price. In the base case all stipends are $0, so total cost equals program cost.
  • forgone_earnings calculates the salary lost while in the program. This is the hidden cost that makes a "funded" PhD expensive: you still lose years of a developer's salary.
  • time_to_competency adds program duration to any post-program ramp. The PhD has a 2-year job market. CCIM's ramp is 0 because the experience IS the competency.
  • expected_outcome passes through the BLS wage proxy for each route's destination role.

💡 Why the PhD uses a different wage proxy

The PhD leads to research and teaching, not development practice. Its outcome wage uses Economics Teachers, Postsecondary (SOC 25-1063, $119,980). The MRED and Experience routes both lead to development roles, so they share the Construction Managers proxy (SOC 11-9021, $106,980).

The routes are not interchangeable. The model names that fork rather than pretending all three land in the same job.

Wire compute_axes() into main and run the model
  • In route_model.py, find the main() function. Locate the blank line after the input table prints.
  • Add these lines at the end of main(), before the function closes:
    axes = compute_axes(routes, BASELINE_WAGE)
    print("--- Four-Axis Table ---")
    print(axes.to_string(index=False))
    print()

What does this addition do?

This calls your new function with the routes DataFrame and the BASELINE_WAGE from config.py, then prints the resulting four-axis table so you can inspect the scores side by side.

  • Save route_model.py.

Before you run this, think: which route do you expect to have the lowest total cost? Which will take the longest to reach competency?

  • Run the model by executing this command in your terminal:
python route_model.py

You should see two tables printed. The second table shows the four-axis scores for each route.

The Experience+CCIM route has the lowest total cost (~$8k) but the longest time to competency (4 years). The MRED has the highest total cost (~$99k) but the shortest time to a development role (1.5 years). No single route wins on every axis.

Not seeing the four-axis table?

  • Make sure compute_axes() is defined outside of main(), between load_routes() and main().
  • Check that the new lines inside main() are indented with 4 spaces, matching the rest of the function body.
  • Confirm your virtual environment is active (you should see (.venv) in your terminal prompt).

Still stuck? Ask for help.

Record the wage-proxy decision and close the ticket

Every modeling choice deserves a written rationale. The wage proxy assignment is a judgment call that changes the model's outputs, so it belongs in your decision record.

  • Open DECISIONS.md and add this entry below the existing Decision 1:
## Decision 2
PhD outcome uses SOC 25-1063 (Economics Teachers, $119,980) because the route leads to research and teaching. MRED and Experience use SOC 11-9021 (Construction Managers, $106,980) because both lead to development practice.
  • Save DECISIONS.md.
  • Commit your changes, push to your branch, and close the Linear ticket via PR by running:
git add .
git commit -m "Add four-axis scoring with compute_axes()"
git push
  • Open your PR in GitHub and merge it to close the corresponding Linear ticket.

Link PRs to Linear tickets

Include the Linear ticket ID (e.g. FIR-2) in your commit message or PR title. Linear auto-closes the ticket when the PR merges.

✔️ Awesome, I've got everything!

Great. Confirm you have saved both route_model.py and DECISIONS.md.

ⓧ I'd like to double check the full code

Your route_model.py is over 30 lines now. Scroll up and confirm these sections are present in order:

  • Imports and config import at the top (unchanged from Step 1).
  • load_routes() function (unchanged from Step 1).
  • compute_axes() function you added in this step (15 lines, returns a DataFrame with route, total_cost, forgone_earnings, time_to_competency, expected_outcome).
  • main() function that prints the input table, then calls compute_axes() and prints the four-axis table.
  • The if __name__ == "__main__": guard at the bottom.

Your DECISIONS.md should now contain two entries: Decision 1 (why Python over a spreadsheet) and Decision 2 (why the PhD uses a different SOC code).

Your model now scores all three routes on identical axes. The numbers show a real tradeoff: cheapest is not fastest, and fastest is not cheapest. Next up, you will run a break-even calculation to find out when each degree route actually pays back its cost.

Break-Even and the Naive Mistake

You have a four-axis table that scores each route on total cost, forgone earnings, time to competency, and expected outcome. But a table of numbers is not a decision. The real question is: when does a degree route's higher earning power pay back its upfront investment?

In this step, you will add a break-even calculator that uses Net Present Value (NPV) to find the year each degree route pays back its cost. You will run the model expecting a clear winner. The result will surprise you.

In this step, get ready to:
  • Add a break-even function that finds the payback year for each degree route against the experience baseline.
  • Run the model and observe whether any route pays back within 50 years.
Add the break-even function

The break-even year is the point where a degree route's cumulative earnings advantage exceeds the investment it required. You will use npf.npv() from numpy-financial to discount future earnings back to present value.

  • In route_model.py, add the following function below compute_axes():
def compute_breakeven(
    axes: pd.DataFrame, baseline_wage: float, discount_rate: float
) -> pd.DataFrame:
    """Compute break-even year for each route against the experience baseline."""
    results = []
    experience_row = axes[axes["route"] == "Experience+CCIM"].iloc[0]
    experience_outcome = experience_row["expected_outcome"]

    for _, row in axes.iterrows():
        if row["route"] == "Experience+CCIM":
            results.append({"route": row["route"], "breakeven_year": "Baseline"})
            continue

        total_investment = row["total_cost"] + row["forgone_earnings"]
        annual_differential = row["expected_outcome"] - experience_outcome

        if annual_differential <= 0:
            results.append({"route": row["route"], "breakeven_year": "Never"})

What does this code do?

The function loops through each route and compares its outcome wage to the Experience+CCIM route's outcome wage. That difference is the annual earnings advantage of choosing the degree.

  • experience_outcome is the baseline earnings (Construction Managers, $106,980). Every degree route is measured against it.
  • total_investment combines tuition AND forgone earnings. This is the full price of leaving the market.
  • If the annual differential is zero or negative, the route never pays back. It goes straight to "Never".
  • Immediately below that code (still inside the for loop's else branch), add the NPV search loop and the return statement:
        else:
            # Find year N where NPV of N years of differential exceeds investment
            found = False
            for year in range(1, 51):
                cashflows = [annual_differential] * year
                pv = npf.npv(discount_rate, cashflows)
                if pv >= total_investment:
                    breakeven_year = row["time_to_competency"] + year
                    results.append({
                        "route": row["route"],
                        "breakeven_year": f"{breakeven_year:.1f}",
                    })
                    found = True
                    break
            if not found:
                results.append({"route": row["route"], "breakeven_year": "Never"})

    return pd.DataFrame(results)

What does the NPV loop do?

For each year from 1 to 50, the loop builds a list of annual cash flows (the earnings differential repeated N times) and discounts them to present value using the 5% discount rate. The first year where that present value exceeds the total investment is the break-even year.

The break-even year adds time_to_competency because the learner only starts earning the differential after completing the program and any ramp-up period.

  • Save route_model.py.

Seeing an IndentationError?

The else: block must align with the if annual_differential <= 0: line above it. Both sit inside the for _, row loop at 8 spaces of indentation.

help me fix my indentation in compute_breakeven

Wire break-even into main and run the model

The function exists but nothing calls it yet. You need to wire it into main() so the break-even table prints alongside the other two tables.

  • In route_model.py, find the end of the main() function where it prints the four-axis table. Add the following code below the last print() call:
    breakeven = compute_breakeven(axes, BASELINE_WAGE, DISCOUNT_RATE)
    print("--- Break-Even Table ---")
    print(breakeven.to_string(index=False))
    print()

What does this code do?

This calls compute_breakeven() with the axes DataFrame, the baseline wage, and the discount rate. It then prints the result as a third table in the terminal output.

  • Save route_model.py.

Before you run this, think about what you expect to see. The PhD earns $119,980 versus the experience route's $106,980. That is a $13,000 annual advantage. The PhD also costs about $99k in tuition plus $535k in forgone earnings (5 years at $107k). Do you think $13k per year is enough to repay $633k?

  • Run the model by executing this command:
python route_model.py

You will see a break-even table showing "Never" for both the PhD and the MRED.

Why does every route show Never?

The MRED and Experience routes have the same outcome wage ($106,980). Their annual differential is $0. Zero dollars per year never repays anything.

The PhD has a $13,000 annual differential, but its total investment is $633,000 (tuition plus forgone earnings). At a 5% discount rate, that differential discounted over 50 years never accumulates enough present value to repay the investment.

If no degree ever pays back, why does anyone get one? This is the right question. The model is telling you something is wrong with the assumptions, not with the degrees. The next step fixes it.

🙋‍♀️ Seeing a NameError or AttributeError?

Check that compute_breakeven is defined between compute_axes() and main() in your file. Python reads top to bottom, so the function must exist before main() calls it.

help me debug my route_model.py error

✔️ Awesome, I've got everything!

Great. Confirm you have saved route_model.py and that your terminal output shows all three tables (Input, Four-Axis, and Break-Even) with both PhD and MRED showing "Never".

ⓧ I'd like to double check the full code

Your route_model.py should contain these sections in order. Scroll up and confirm each is present:

  • Imports (sys, pandas, numpy, numpy_financial) and the config import.
  • load_routes() returning the three-route DataFrame.
  • compute_axes() computing total_cost, forgone_earnings, time_to_competency, and expected_outcome.
  • compute_breakeven() with the NPV loop returning "Baseline", a year, or "Never" for each route.
  • main() calling all three functions and printing the Input Table, Four-Axis Table, and Break-Even Table.
Commit and close the ticket
  • Stage, commit, and push your changes by running these commands:
git add .
git commit -m "Add break-even calculator showing Never for both degree routes"
git push
  • Open a pull request in GitHub and merge it.
  • Close the corresponding Linear ticket for this step.

The model just told you that no degree route ever pays back. That cannot be right. The problem is not the math. The problem is the assumptions: an unfunded PhD and a generic BLS wage for the MRED. Next up, you will fix both assumptions and watch the recommendation flip.

Sensitivity, the Flip, and the Decision Brief

The previous step ended with a troubling result: both degree routes showed "Never" for break-even. That "Never" is not a bug in the code. It exposes two modeling errors in the assumptions.

The PhD is usually funded ($43k stipend, tuition waived), which drops its total investment from $633k to $105k. The MRED outcome wage should use the USC-reported $124k graduate figure rather than the generic BLS proxy. In this step, you fix both, prove the model is responsive, and write a decision brief with a recommendation you can defend.

In this step, get ready to:
  • Add a sensitivity function that models the PhD as funded and raises the MRED outcome wage.
  • Write a decision brief generator that outputs a recommendation with reversal conditions.
  • Add a reconciliation check that traces the MRED's arithmetic to a hand calculation.
Add the funded sensitivity function

The base case modeled the PhD as unfunded to expose the break-even problem. Now you add an alternative loader that reflects how most PhDs actually work: fully funded with a stipend.

  • In route_model.py, add the following function directly below load_routes():
def load_routes_funded_phd() -> pd.DataFrame:
    """Load routes with the PhD modeled as fully funded and MRED outcome adjusted."""
    df = load_routes()
    phd_mask = df["route"] == "PhD"
    df.loc[phd_mask, "program_cost"] = 0
    df.loc[phd_mask, "annual_stipend"] = 43_000
    df.loc[phd_mask, "source"] = (
        "UF Warrington PhD: $43,000 stipend + tuition waiver, 5yr; "
        "BLS SOC 25-1063 Economics Teachers $119,980 (May 2024)"
    )
    mred_mask = df["route"] == "MRED"
    df.loc[mred_mask, "outcome_annual_wage"] = 123_932
    df.loc[mred_mask, "source"] = (
        "MIT MSRED: $98,662 (2026-27); USC Price MRED reported $123,932 1yr post-completion"
    )
    return df

What does this function do?

It starts from the base route data and overrides two assumptions.

  • The PhD gets its program cost zeroed out and a $43,000 annual stipend applied (matching UF Warrington's published package).
  • The MRED outcome wage rises to $123,932, using USC Price's program-reported figure instead of the generic BLS proxy.
  • Both source strings update to document where the new numbers come from.

Now the model needs a way to choose between the base case and the funded sensitivity. You add a --funded flag to main() so the learner can switch between assumptions from the terminal.

  • Find the existing main() function in route_model.py and replace it entirely with this updated version:
def main():
    funded = "--funded" in sys.argv

    if funded:
        print("=== SENSITIVITY RUN: PhD funded, MRED outcome at USC-reported ===\n")
        routes = load_routes_funded_phd()
    else:
        print("=== BASE CASE: PhD unfunded, generic BLS outcomes ===\n")
        routes = load_routes()

    print("--- Input Table ---")
    print(routes[["route", "program_cost", "duration_years", "annual_stipend", "outcome_annual_wage"]].to_string(index=False))
    print()

    axes = compute_axes(routes, BASELINE_WAGE)
    print("--- Four-Axis Table ---")
    print(axes.to_string(index=False))
    print()

    breakeven = compute_breakeven(axes, BASELINE_WAGE, DISCOUNT_RATE)
    print("--- Break-Even Table ---")
    print(breakeven.to_string(index=False))
    print()

What changed in main()?

The first line checks sys.argv for the --funded flag. If present, it loads the funded sensitivity data instead of the base case.

A header line prints before any tables so you always know which scenario is running. The rest of the function (axes, break-even) is unchanged. You will add reconcile() and write_brief() calls later in this step.

  • Save route_model.py.
  • Before you run this, what do you think the PhD break-even will show with the funded assumptions?
  • Run the sensitivity mode by running:
python route_model.py --funded

You should see the sensitivity header and the PhD row now showing program_cost of 0 and annual_stipend of 43000. The break-even table now shows 18.0 for the PhD and 31.5 for the MRED instead of "Never".

  • Confirm the base case still works by running:
python route_model.py

Both degree routes should still show "Never" in the base case. The sensitivity flag controls which assumptions the model uses.

Break-even still shows Never in funded mode?

  • Check that load_routes_funded_phd() sets annual_stipend to 43_000, not 43. Missing the underscore thousands separator is fine, but the value must be 43000.
  • Verify mred_mask sets outcome_annual_wage to 123_932. If it is still 106_980, the MRED differential is zero and break-even stays "Never".
  • Make sure main() calls load_routes_funded_phd() when funded is True, not load_routes().

help me debug why my funded PhD break-even still shows Never

Add the decision brief writer

The model now computes different results depending on the funding assumption. The next piece is a function that renders those results into a one-page markdown brief with a recommendation, a break-even table, and named reversal conditions.

What is the decide-and-defend pattern?

The output is a recommendation you can stand behind, not a neutral table that leaves the choice unmade. The brief names which route wins at your time horizon, why, and what specific input change would flip the answer.

A table lets a decision stay deferred forever. A brief forces a choice and makes the reversal conditions explicit.

  • In route_model.py, add the first part of the write_brief() function below compute_breakeven(). This chunk determines which route to recommend:
def write_brief(
    routes: pd.DataFrame,
    axes: pd.DataFrame,
    breakeven: pd.DataFrame,
    funded: bool,
) -> str:
    """Generate the one-page decision brief as markdown."""
    has_finite = breakeven[
        ~breakeven["breakeven_year"].isin(["Baseline", "Never"])
    ]

    if has_finite.empty:
        recommendation = "Experience+CCIM"
        reasoning = (
            "No degree route breaks even within 50 years at the given assumptions. "
            "The experience route has the lowest total cost and delivers competency "
            "through deals, which is how most developers actually learn."
        )
    else:
        best_route = has_finite.iloc[0]["route"]
        recommendation = best_route
        be_year = has_finite.iloc[0]["breakeven_year"]
        reasoning = (
            f"The {best_route} route breaks even at year {be_year} "
            f"and delivers a credentialing premium over the experience baseline."
        )

What does the recommendation logic do?

  • It filters the break-even table for routes that have a finite break-even year (not "Baseline" or "Never").
  • If no route breaks even, it recommends Experience+CCIM as the default.
  • If at least one route has a finite break-even, it picks the first one and constructs a reasoning string naming the year.
  • Continue the write_brief() function by adding the brief header, recommendation section, and the two data tables directly below the code you just added:
    brief = f"""# Route-Decision Brief: Real Estate Development

*Generated from published data. Not career, investment, legal, or tax advice.*
*Every figure is illustrative and dated. Verify before quoting.*

## Recommendation

At a {TIME_HORIZON_YEARS}-year horizon: **{recommendation}**

{reasoning}

## Break-Even Against Experience+CCIM Baseline

| Route | Break-Even Year |
|-------|----------------|
"""
    for _, row in breakeven.iterrows():
        brief += f"| {row['route']} | {row['breakeven_year']} |\n"

    brief += """
## Four-Axis Comparison

| Route | Total Cost | Forgone Earnings | Time to Competency | Expected Outcome |
|-------|-----------|-----------------|-------------------|------------------|
"""
    for _, row in axes.iterrows():
        brief += (
            f"| {row['route']} | ${row['total_cost']:,.0f} | "
            f"${row['forgone_earnings']:,.0f} | "
            f"{row['time_to_competency']:.1f} yr | "
            f"${row['expected_outcome']:,.0f} |\n"
        )

What does this template build?

The f-string creates the markdown header with the recommendation statement. Then two loops iterate over the break-even and axes DataFrames to build the markdown tables row by row.

Each number uses Python's format specifiers: :,.0f for comma-separated dollars and :.1f for one decimal year.

  • Add the final section of write_brief() with the reversal conditions, routes explanation, sources, caveats, and return statement:
    brief += f"""
## Reversal Conditions

The recommendation reverses if:
- The PhD is modeled as {"unfunded (tuition ~$99k, no stipend)" if funded else "fully funded ($43k stipend + tuition waiver)"}: changes PhD total cost and break-even.
- The MRED outcome wage {"drops to the generic BLS proxy ($107k)" if funded else "rises to the USC-reported $124k"}: changes whether the MRED breaks even.
- The experience route's time to competency shortens below 2 years: eliminates any time advantage of the MRED.
- The discount rate changes materially: a higher rate extends all break-even years.

## The Routes Are Not Interchangeable

- The PhD leads to research and teaching, not development practice.
- The MRED teaches the development craft directly in 1-2 years.
- The experience route (deals + CCIM) is how most developers actually learn, at the lowest tuition cost and the longest time to a guarantee.

A standalone development doctorate barely exists. The model says so rather than inventing one.

## Sources

"""
    for _, row in routes.iterrows():
        brief += f"- {row['route']}: {row['source']}\n"

    brief += """
## Caveats

- Expected outcomes are estimates from a wage distribution, not a promise.
- Time to competency is a planning figure, not a guarantee.
- Cost is not tuition: a funded PhD's real cost is years of a developer's salary not earned.
- Program costs and wages change every year. Reload with current figures before committing.
- This model confers a choice made with evidence, not a degree, a license, or a guaranteed salary.
"""
    return brief

What are the reversal conditions?

Each bullet names a specific input change that would flip the recommendation. The conditions use Python's inline ternary to swap the text depending on whether you ran the funded or unfunded scenario.

The "Routes Are Not Interchangeable" section is a key insight: the PhD leads to research and teaching, the MRED leads to development practice, and the experience route is how most developers actually learn. They are not three flavors of the same thing.

Now wire write_brief() into main() so it runs on every execution and writes the brief to disk.

  • In main(), find the line print(breakeven.to_string(index=False)) and the print() that follows it. Add these lines at the end of main(), after that final print():
    brief = write_brief(routes, axes, breakeven, funded)
    with open("decision_brief.md", "w", encoding="utf-8") as f:
        f.write(brief)
    print("Brief written to decision_brief.md")
  • Save route_model.py.
  • Run the funded sensitivity to verify the brief is generated:
python route_model.py --funded

You should see all three tables printed, followed by "Brief written to decision_brief.md".

  • Open decision_brief.md and confirm it contains a recommendation, the break-even table, the four-axis comparison, and reversal conditions.

Seeing a NameError for write_brief?

  • Make sure write_brief() is defined ABOVE main() in the file. Python reads top to bottom, so main() must see the function definition before calling it.
  • Check that all three code chunks for write_brief() are pasted as one continuous function with consistent indentation (4 spaces throughout).

help me fix a NameError or IndentationError in my write_brief function

Add reconciliation and finalize the model

A model you cannot trace to a hand calculation is a model you cannot trust. The reconciliation function picks one route and asserts that the model's computed value matches simple arithmetic.

  • In route_model.py, add the reconcile() function below compute_breakeven() (and above write_brief()):
def reconcile(routes: pd.DataFrame, axes: pd.DataFrame, baseline_wage: float) -> None:
    """Reconcile the MRED row to a hand calculation."""
    mred_route = routes[routes["route"] == "MRED"].iloc[0]
    mred_axes = axes[axes["route"] == "MRED"].iloc[0]

    hand_forgone = (
        baseline_wage * mred_route["duration_years"]
        - mred_route["annual_stipend"] * mred_route["duration_years"]
    )
    model_forgone = mred_axes["forgone_earnings"]

    assert abs(hand_forgone - model_forgone) < 0.01, (
        f"Reconciliation failed: hand={hand_forgone}, model={model_forgone}"
    )
    print("Reconciliation passed: MRED forgone earnings match hand calculation.")
    print(f"  Hand: {BASELINE_WAGE} * {mred_route['duration_years']} - "
          f"{mred_route['annual_stipend']} * {mred_route['duration_years']} = ${hand_forgone:,.0f}")

What does reconcile() prove?

It computes the MRED's forgone earnings by hand: baseline wage times duration, minus stipend times duration. Then it asserts that this hand calculation matches what compute_axes() produced.

If someone accidentally changes a formula, this assertion catches the drift before the brief goes out with wrong numbers.

  • In main(), find the last print() after the break-even table. Add these two lines immediately after it, before the brief = write_brief(...) line:
    reconcile(routes, axes, BASELINE_WAGE)
    print()
  • Save route_model.py.
  • What do you expect to see when you run the funded mode now?
  • Run the full model in funded mode:
python route_model.py --funded

You should see the sensitivity header, all three tables, "Reconciliation passed: MRED forgone earnings match hand calculation" with the arithmetic shown, and "Brief written to decision_brief.md".

  • Run again without the flag to confirm the base case still works:
python route_model.py

The base case should show "Never" for both degree routes, reconciliation passed, and a brief written that recommends Experience+CCIM.

Seeing AssertionError from reconcile()?

  • This means compute_axes() is producing a different value than the hand formula. Check that your forgone_earnings formula in compute_axes() is baseline_wage * duration_years - annual_stipend * duration_years.
  • Confirm BASELINE_WAGE in config.py is 106_980.

help me debug why reconcile() is failing with an AssertionError

Now update DECISIONS.md with the third decision recording where the sensitivity numbers come from.

  • Open DECISIONS.md and add this entry below Decision 2:
## Decision 3
Funded PhD sensitivity uses UF Warrington's published $43,000 stipend. MRED sensitivity uses USC Price's program-reported $123,932 one-year-out figure.
  • Save DECISIONS.md.
  • Commit, push, and close the Linear ticket via PR by running:
git add .
git commit -m "Add sensitivity, reconciliation, and decision brief"
git push
  • Create a PR in GitHub and merge it to close the Linear ticket.
  • Tag the release by running:
git tag v1.0.0
git push origin v1.0.0

Why tag a release?

A tagged release marks the model as complete and runnable. Anyone (including future-you) can check out v1.0.0 and reproduce the analysis from that point.

✔️ Awesome, I've got everything!

Great. Double check you have saved both route_model.py and DECISIONS.md.

ⓧ I'd like to double check the full code

Your route_model.py is now quite long. Scroll through your file and confirm these functions appear in order, each fully intact:

  • load_routes() returning the base DataFrame with three routes.
  • load_routes_funded_phd() overriding PhD cost/stipend and MRED outcome wage.
  • compute_axes() computing total_cost, forgone_earnings, time_to_competency, expected_outcome.
  • compute_breakeven() finding the break-even year for each degree route.
  • reconcile() asserting MRED forgone earnings match a hand calculation.
  • write_brief() generating the full markdown brief with recommendation, tables, reversal conditions, sources, and caveats.
  • main() with the --funded flag, calling all of the above and writing decision_brief.md.

Your DECISIONS.md should have three entries: the spreadsheet choice, the SOC codes choice, and the funded sensitivity sources.

The model is now complete: parameterized, reconciled, and documented. Next up, you will build the topology diagram, the plain-language guide, and the two-audience walkthrough that make the repo usable by anyone who picks it up.

Topology, Documentation, and Walkthrough

Your model computes, reconciles, and writes a decision brief. But if someone picks up this repo next year, they have no map of how the pieces connect and no instructions for updating the inputs when program costs change.

In this step you document the model so it can outlive your memory. A topology diagram makes the agent roster visible, a plain-language guide explains how to rebuild and update the model, and a two-audience walkthrough closes the loop for anyone who reads the repo.

In this step, get ready to:
  • Create a Mermaid topology diagram showing the agent roster and data flow.
  • Write a plain-language guide covering what the model is, how to rebuild it, and how to update it.
  • Create a two-audience walkthrough with lessons learned, then commit and close the Linear ticket.
Create the topology diagram

A topology diagram turns an invisible architecture into something a reader can point at. Yours will show every agent in the model and how data flows between them, rendered as a Mermaid flowchart.

  • Create topology.md in your first-choice folder and paste the Mermaid flowchart diagram:
# Agent Topology

```mermaid
flowchart TD
    PD[Program Data Loader] --> AC[Axis Calculators]
    BLS[BLS Wage Loader] --> AC
    AC --> BE[Break-Even Calculator]
    BE --> SR[Sensitivity Runner]
    BE --> BW[Brief Writer]
    SR --> BW
    AC --> RC[Reconciler]
    BW --> OUT[decision_brief.md]
    RC --> PASS[Reconciliation Passed]
    TB[Teach-Back Scorer] -.-> OUT
```

What does this diagram show?

Each node is an agent (a function or logical unit) in the model. Solid arrows show data flowing from loaders to calculators to outputs. The dashed arrow from Teach-Back Scorer indicates it reads the brief but is not part of the main computation pipeline.

  • Below the closing code fence, add the agent roster table and delivery pipeline description:
## Agent Roster

| Agent | Responsibility | Input | Output |
|-------|---------------|-------|--------|
| Program Data Loader | Load cost, duration, stipend from published program pages | Program URLs | Routes DataFrame |
| BLS Wage Loader | Load outcome wages by SOC code from BLS OEWS | SOC codes | Wage column in DataFrame |
| Axis Calculators (x4) | Compute total cost, forgone earnings, time to competency, expected outcome | Routes + Config | Axes DataFrame |
| Break-Even Calculator | Find year each degree route pays back vs. experience baseline (discounted via npf.npv) | Axes + discount rate | Break-even table |
| Sensitivity Runner | Override inputs and recompute to test responsiveness | Routes + override params | Sensitivity results |
| Brief Writer | Render recommendation, break-even, and reversal conditions as markdown | Axes + Break-even | decision_brief.md |
| Reconciler | Assert one route's arithmetic matches hand calculation | Routes + Axes | Pass/Fail |
| Teach-Back Scorer | Quiz learner on recommendation and reversal conditions | Model outputs + learner input | Score (0-3) |

## Delivery Pipeline

Private GitHub repo. Linear tickets per build step. Branch-per-ticket, PR closes ticket on merge. Tagged release at completion. Plain-language repo docs (guide.md, DECISIONS.md, WALKTHROUGH.md).

What does the roster table capture?

Each row names one agent, what it does, what it takes in, and what it produces. This makes it trivial to find the right function when a value seems wrong. The delivery pipeline section documents the workflow (branch, PR, merge, tag) so a future reader knows the process, not just the code.

  • Save topology.md.
  • Open https://mermaid.live in your browser.
  • Select all the default code in the Code panel on the left and delete it.
  • Paste only the Mermaid syntax (the lines between the ```mermaid and ``` fences) into the Code panel.

The Preview panel on the right renders your flowchart in real time. You should see eight labeled nodes connected by arrows, with the data flowing top-down from the loaders through the calculators to the outputs.

Seeing a syntax error in the preview?

Make sure you pasted only the lines starting with flowchart TD through the last arrow line. Do not include the triple-backtick fences themselves.

Check for missing or extra spaces around the arrow operators (--> and -.->).

help me fix a Mermaid syntax error in my topology diagram

✔️ Awesome, I see the flowchart rendered!

Great. Your topology diagram is complete and verified.

ⓧ I'd like to double check the full code

Your topology.md should contain two sections in this order:

  • The # Agent Topology heading followed by the Mermaid code fence with the flowchart TD block (10 nodes and edges).
  • The ## Agent Roster heading followed by the 8-row markdown table.
  • The ## Delivery Pipeline heading followed by one paragraph describing the branch-per-ticket workflow.

Scroll up and confirm each section is present and in order.

Write the plain-language guide

The guide is documentation for someone who needs to understand, rebuild, or update the model without reading every line of code. It covers what the model is, why the routes differ, and how to keep the inputs current.

  • Create guide.md in your first-choice folder and add the model description and route definitions:
# Plain-Language Guide: The Route-Decision Model

## What This Model Is

A decision-support study that compares three routes into real estate development
on the same four axes, from published data. It is not career, investment, legal,
or tax advice. Expected outcomes are estimates from a wage distribution, not a
promise. The model confers a choice made with evidence, not a degree, a license,
or a guaranteed salary.

## The Three Routes (Not Interchangeable)

1. **PhD** (Real Estate Economics / Finance / Urban Planning): 4-6 years, often
   funded. Leads to research and teaching, not development practice. A standalone
   development doctorate barely exists.
2. **MRED / MSRED** (Master of Real Estate Development): 1-2 years at real
   tuition. Teaches the development craft directly.
3. **Experience + CCIM** (Deals plus the Certified Commercial Investment Member
   designation): ~$8k in coursework, 1+ year of classes, but requires 2+ years
   of qualifying deal experience. How most developers actually learn.

Why state what the model is not?

The opening section draws a clear boundary: this is a decision-support study, not advice. That framing protects the reader from treating illustrative estimates as promises. The route descriptions explain why the three paths cannot be swapped freely.

  • Below the routes section, add the method patterns and foundations corrections:
## Three Method Patterns

- **Same-axes scoring**: All three routes are scored on identical axes. A
  comparison across different measures is not a comparison.
- **Forgone-earnings discipline**: The hidden cost of a route is the earning it
  displaces. Forgone earnings sit next to tuition on every route. This is what
  stops a funded PhD from looking free.
- **Decide-and-defend**: The output is a recommendation with named reversal
  conditions, not a neutral table. A table lets the choice stay unmade.

## Three Foundations Misreadings Corrected

- **Cost is not tuition**: A funded PhD's real cost is years of a developer's
  salary not earned. The model counts it.
- **The routes are not interchangeable**: The PhD is for research and teaching.
  The MRED plus deals is how you learn to develop. The model names that fork.
- **A decision is not a table**: The deliverable is a recommendation the learner
  can defend, not a spreadsheet that leaves the choice open.

What do the method patterns communicate?

These three patterns are the intellectual scaffolding of the model. Same-axes scoring ensures a fair comparison. Forgone-earnings discipline makes hidden costs visible. Decide-and-defend commits to an answer rather than leaving the reader with a table and no conclusion.

  • Save guide.md and open it in your editor's preview (or any markdown viewer).

You should see four rendered headings so far: the title, What This Model Is, The Three Routes, Three Method Patterns, and Three Foundations Misreadings Corrected.

  • Add the four axes definitions, rebuild instructions, and update guide:
## The Four Axes

- **Total Cost**: Program tuition + fees, minus any stipend received.
- **Forgone Earnings**: The wage you would have earned during the program,
  minus any stipend. This is the real cost of time out of the market.
- **Time to Competency**: Program duration plus any post-program ramp
  (job market for PhD, portfolio requirement for CCIM).
- **Expected Outcome**: The BLS median wage for the role the route leads to,
  using a named proxy SOC code where no exact title exists.

## How to Rebuild It

1. Install Python 3.11+, pandas, numpy, numpy-financial.
2. Create `config.py` with BASELINE_WAGE, DISCOUNT_RATE, TIME_HORIZON_YEARS.
3. Create `route_model.py` with load_routes(), compute_axes(),
   compute_breakeven(), reconcile(), and write_brief().
4. Run: `python route_model.py` for base case, `python route_model.py --funded`
   for sensitivity.

## How to Update When Costs or Wages Change

- **Program costs**: Replace `program_cost` values in `load_routes()`. Find
  current figures on program tuition pages (MIT CRE, USC Price, CCIM Institute).
- **Wages**: Replace `outcome_annual_wage` and BASELINE_WAGE in config. Find
  current figures at bls.gov/ooh under the relevant occupation.
- **Duration**: Replace `duration_years` if program lengths change.
- **Always update the source string** when you change a value.

Why include rebuild and update instructions?

Program costs and BLS wages change every year. These sections tell a future reader exactly where to go and which values to replace, so the model stays current without reverse-engineering the code.

  • Finish guide.md with the adoption framing, reference links, and verify-before-quoting note:
## Adoption: The Model Stays Live

Before committing to a route, reload the model with real, current program costs
and wages and re-run the break-even. Each real figure sharpens the answer. By
the time the underwriting and market waves are built, you have already committed
to a route with evidence, so you learn the later craft on a foundation you chose
rather than drifted into.

## Where the Deeper Material Lives

- BLS Occupational Outlook Handbook: https://www.bls.gov/ooh/
- BLS OEWS Tables: https://www.bls.gov/oes/
- MIT Center for Real Estate: https://cre.mit.edu/
- USC Price MRED: https://priceschool.usc.edu/academics/masters/master-of-real-estate-development-mred/
- CCIM Institute: https://www.ccim.com/education/pursue-ccim-designation/designation-program-time-cost

## Verify Before Quoting

Every figure in this model is dated. Program costs change annually. BLS wages
update each May. Before using any number in a real decision, visit the source
URL and confirm the current figure.
  • Save guide.md and scroll through the full file in your editor.

You should see nine section headings in total, ending with Verify Before Quoting. Every section is present and the file reads as a self-contained document.

Missing a section in guide.md?

Check that each chunk was appended below the previous one, not pasted over it. The file should have one # heading at the top and eight ## headings below.

help me check my guide.md structure

✔️ All nine sections are present!

Your guide is complete. Save the file and move on.

ⓧ I'd like to double check the full code

Your guide.md should contain these sections in order:

  • Title (# Plain-Language Guide: The Route-Decision Model).
  • What This Model Is (paragraph with the caveat).
  • The Three Routes (numbered list of PhD, MRED, Experience+CCIM).
  • Three Method Patterns (same-axes, forgone-earnings, decide-and-defend).
  • Three Foundations Misreadings Corrected (cost, routes, decision).
  • The Four Axes (Total Cost, Forgone Earnings, Time to Competency, Expected Outcome).
  • How to Rebuild It (4-step numbered list).
  • How to Update When Costs or Wages Change (4 bullet items).
  • Adoption: The Model Stays Live (one paragraph).
  • Where the Deeper Material Lives (5 URLs).
  • Verify Before Quoting (closing paragraph).

Scroll up and confirm each chunk is present in order.

Create the walkthrough and close the ticket

The walkthrough addresses two audiences. For the mentor who asked for the build, it proves the model delivers a recommendation, not an opinion. For a future learner picking up the repo, it maps the files and names the three lessons.

  • Create WALKTHROUGH.md in your first-choice folder and add the mentor section:
# Walkthrough: The First Choice

## For the Mentor

This build delivers what you asked for: a model, not an opinion.

The route-decision model scores three entry paths into real estate development
on the same four axes, from published data. The output is a recommendation the
learner can defend, with named reversal conditions. It is not a table that
leaves the choice open.

Key deliverables:
- A parameterized Python model that recomputes on every run.
- A one-page decision brief with recommendation, break-even, and reversal conditions.
- A sensitivity proof that the recommendation flips under a stated input change.
- A reconciliation check that traces arithmetic to a hand calculation.

Why lead with the mentor audience?

The mentor asked for a model that delivers a defensible recommendation. This section confirms the ask was met: the output is a choice backed by arithmetic, not a spreadsheet that leaves the decision unmade.

  • Below the mentor section, add the future learner section with file descriptions and lessons:
## For a Future Learner

### How to Pick Up This Repo

1. Clone the repo and activate the virtual environment.
2. Run `python route_model.py` for the base case.
3. Run `python route_model.py --funded` for the sensitivity case.
4. Read `decision_brief.md` for the recommendation.

### What Each File Does

- `config.py`: Baseline assumptions (discount rate, baseline wage, time horizon).
- `route_model.py`: The model (load, compute, break-even, reconcile, write brief).
- `decision_brief.md`: The output brief, regenerated on every run.
- `topology.md`: The agent roster and data flow diagram.
- `guide.md`: Plain-language documentation for rebuilding and updating.
- `DECISIONS.md`: Decision record capturing each modeling choice.
- `WALKTHROUGH.md`: This file.

### Three Lessons

1. **Cost is not tuition.** A funded PhD's real cost is years of a developer's
   salary not earned. The model counts forgone earnings next to tuition.
2. **The routes are not interchangeable.** The PhD leads to research and
   teaching. The MRED teaches the development craft. The experience route is
   how most developers actually learn.
3. **A decision is not a table.** The deliverable is a recommendation the
   learner can defend, not a neutral spreadsheet. The model picks a winner
   and names what would flip it.

What do the three lessons capture?

These are the conceptual takeaways the model teaches through arithmetic. Cost is not tuition (forgone earnings dominate). The routes fork into different careers. A model must commit to a recommendation and name what reverses it.

  • Save WALKTHROUGH.md.
  • Open the file and confirm it has two top-level sections (For the Mentor, For a Future Learner) and that the Three Lessons heading appears at the bottom with all three numbered items.

Walkthrough missing a section?

Confirm the second code chunk was appended below the first, not pasted over it. The file should start with # Walkthrough: The First Choice and contain two ## headings.

help me verify my WALKTHROUGH.md structure

Before you run the next command, what do you expect to see when you list the files in your project folder?

  • Confirm all documentation files exist by running:
ls *.md

You should see DECISIONS.md, WALKTHROUGH.md, decision_brief.md, guide.md, and topology.md all listed.

  • Commit all three new files and push by running:
git add topology.md guide.md WALKTHROUGH.md
git commit -m "Add topology, guide, and walkthrough documentation"
git push
  • Return to your Linear project and close the documentation ticket via the PR.

Your model is now fully documented. The topology, guide, walkthrough, and decision record mean anyone picking up this repo can understand, rebuild, and update the model without needing you in the room.

Secret mission

The Teach-Back Instrument

A model you cannot explain is a model you do not own. Build a teach-back instrument that quizzes you on your own model's outputs, scoring whether you can defend the recommendation and name what would reverse it.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with a private GitHub repo and a Linear project. There are no paid cloud resources or persistent services, so nothing here incurs ongoing costs.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder first-choice on your Desktop (contains all Python files, the virtual environment, and generated outputs).
  • Private GitHub repo first-choice with tagged release.
  • Linear project with tickets and closed PRs.

Keep everything running

No action needed. Choose this if you plan to reload the model with real current figures before committing to a route, or if you want to extend the model with additional sensitivity runs.

  • Your local first-choice folder stays on your Desktop, ready to run anytime.
  • Your GitHub repo persists at no cost (private repos on the free plan have no charge).
  • Your Linear project persists at no cost on the free plan.

Pause - I'll come back to this later

Shut down the virtual environment to free up your terminal, but keep the files for later.

  • Deactivate your virtual environment by running:
deactivate
  • Your local files, GitHub repo, and Linear project all remain intact for when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Deactivate your virtual environment (if still active) and delete the local project folder. On macOS or Linux, run:
deactivate
cd ~/Desktop
rm -rf first-choice
  • On Windows, run this instead:
deactivate
cd ~\Desktop
rmdir /s /q first-choice
  • Delete the GitHub repo: open your first-choice repo on GitHub, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Confirm by typing the repository name when prompted.
  • Delete the Linear project: open your project in Linear, click the three-dot menu next to the project name, and select Delete. Confirm when prompted.

Well Done!

Well Done!

You turned "which route into development?" from three opinions into arithmetic. You now have a validated decision model, a one-page brief you can hand to a mentor, and the confidence to say exactly what the numbers show and what would change the answer.

Here's what you accomplished:

  • Built a three-route decision model scoring the PhD, MRED, and Experience+CCIM paths on the same four axes (total cost, forgone earnings, time to competency, expected outcome) from published BLS and program data, with a break-even calculator that proved cost alone does not determine the best route.
  • Proved the model is responsive through sensitivity analysis, flipping the PhD from unfunded to funded and watching the recommendation change. Then produced a one-page decision brief with a recommendation you can defend, a break-even table, and named reversal conditions.
  • Delivered the entire model through a private GitHub repo with Linear-tracked tickets, branch-by-branch PRs, a tagged release, and full documentation including a topology diagram, a plain-language rebuild guide, a decision record, and a two-audience walkthrough.
  • Secret Mission: Built a teach-back instrument that scores whether you can defend the recommendation and name what would reverse it.

Ready to quiz yourself?