Most published rent-to-income ratios use overall median household income, which includes homeowners. Redo the math for the largest markets using renter median household income as the denominator instead, and show me how much the affordability picture changes market by market. Where is the standard measure most misleading?
Now let me pull the top 20 largest markets by population along with income and rent data.
mkdir -p /vercel/sandbox/work && cat > /vercel/sandbox/work/rti.py << 'EOF'
import json
data = [
{"name":"New York, NY","population":19940274,"hhi_median":110090,"hhi_owner_median":154779,"hhi_renter_median":72142,"rent":4095.09},
{"name":"Los Angeles, CA","population":12927614,"hhi_median":103873,"hhi_owner_median":143433,"hhi_renter_median":75879,"rent":2799.83},
{"name":"Chicago, IL","population":9406924,"hhi_median":99645,"hhi_owner_median":124825,"hhi_renter_median":61308,"rent":2215.02},
{"name":"Dallas-Fort Worth, TX","population":8344032,"hhi_median":100686,"hhi_owner_median":128604,"hhi_renter_median":66870,"rent":1551.36},
{"name":"Houston, TX","population":7796182,"hhi_median":88043,"hhi_owner_median":119573,"hhi_renter_median":57581,"rent":1448.99},
{"name":"Miami, FL","population":6457988,"hhi_median":88838,"hhi_owner_median":104752,"hhi_renter_median":62069,"rent":2687.99},
{"name":"Washington, DC","population":6437907,"hhi_median":131504,"hhi_owner_median":168604,"hhi_renter_median":85342,"rent":2252.52},
{"name":"Atlanta, GA","population":6409047,"hhi_median":99391,"hhi_owner_median":119764,"hhi_renter_median":63164,"rent":1662.19},
{"name":"Philadelphia, PA","population":6330422,"hhi_median":97580,"hhi_owner_median":124645,"hhi_renter_median":58646,"rent":1974.77},
{"name":"Phoenix, AZ","population":5186958,"hhi_median":97859,"hhi_owner_median":113671,"hhi_renter_median":68403,"rent":1596.72},
{"name":"Boston, MA","population":5025517,"hhi_median":124803,"hhi_owner_median":159360,"hhi_renter_median":74832,"rent":3067.81},
{"name":"Riverside, CA","population":4744214,"hhi_median":96758,"hhi_owner_median":114019,"hhi_renter_median":66091,"rent":2312.15},
{"name":"San Francisco, CA","population":4648486,"hhi_median":150999,"hhi_owner_median":196499,"hhi_renter_median":106164,"rent":3095.46},
{"name":"Detroit, MI","population":4400578,"hhi_median":80496,"hhi_owner_median":99093,"hhi_renter_median":47200,"rent":1470.31},
{"name":"Seattle, WA","population":4145494,"hhi_median":121441,"hhi_owner_median":156633,"hhi_renter_median":84938,"rent":2159.63},
{"name":"Minneapolis-St. Paul, MN","population":3757952,"hhi_median":103154,"hhi_owner_median":128955,"hhi_renter_median":60336,"rent":1661.75},
{"name":"Tampa, FL","population":3424560,"hhi_median":83724,"hhi_owner_median":94301,"hhi_renter_median":58257,"rent":1866.03},
{"name":"San Diego, CA","population":3298799,"hhi_median":116460,"hhi_owner_median":147253,"hhi_renter_median":83254,"rent":2894.92},
{"name":"Denver, CO","population":3050512,"hhi_median":117176,"hhi_owner_median":144411,"hhi_renter_median":76449,"rent":1818.95},
{"name":"Orlando, FL","population":2940513,"hhi_median":87352,"hhi_owner_median":102635,"hhi_renter_median":62377,"rent":1765.38},
{"name":"Charlotte, NC","population":2883370,"hhi_median":93550,"hhi_owner_median":111486,"hhi_renter_median":61971,"rent":1624.69},
{"name":"Baltimore, MD","population":2859024,"hhi_median":104831,"hhi_owner_median":133842,"hhi_renter_median":62321,"rent":1786.40},
{"name":"St. Louis, MO","population":2809527,"hhi_median":87170,"hhi_owner_median":106309,"hhi_renter_median":50166,"rent":1413.55},
{"name":"San Antonio, TX","population":2763006,"hhi_median":83471,"hhi_owner_median":104627,"hhi_renter_median":54067,"rent":1297.87},
{"name":"Austin, TX","population":2550637,"hhi_median":113668,"hhi_owner_median":150393,"hhi_renter_median":77956,"rent":1502.40},
]
def band(r):
if r < 0.30: return "affordable"
if r < 0.40: return "moderate burden"
return "severe burden"
results = []
for d in data:
annual_rent = d["rent"]*12
std_rti = annual_rent/d["hhi_median"]
renter_rti = annual_rent/d["hhi_renter_median"]
gap_pp = (renter_rti - std_rti)*100
results.append({
"name": d["name"],
"population": d["population"],
"rent": d["rent"],
"hhi_median": d["hhi_median"],
"hhi_renter_median": d["hhi_renter_median"],
"renter_income_discount_pct": (1 - d["hhi_renter_median"]/d["hhi_median"])*100,
"std_rti": std_rti,
"renter_rti": renter_rti,
"gap_pp": gap_pp,
"std_band": band(std_rti),
"renter_band": band(renter_rti),
"band_changed": band(std_rti) != band(renter_rti),
})
results.sort(key=lambda x: -x["gap_pp"])
for r in results:
print(f"{r['name']:28s} std={r['std_rti']*100:5.1f}% renter={r['renter_rti']*100:5.1f}% gap={r['gap_pp']:5.1f}pp income_gap={r['renter_income_discount_pct']:5.1f}% {r['std_band']:15s}->{r['renter_band']:15s} changed={r['band_changed']}")
print()
print("National avg std RTI:", sum(r['std_rti'] for r in results)/len(results)*100)
print("National avg renter RTI:", sum(r['renter_rti'] for r in results)/len(results)*100)
with open('/vercel/sandbox/work/results.json','w') as f:
json.dump(results, f, indent=2)
EOF
python3 /vercel/sandbox/work/rti.pycat > /vercel/sandbox/work/rti2.py << 'EOF'
import json
# published rent_to_income_ratio (standard measure, using overall median HHI) directly from datamart
data = [
{"name":"New York, NY","population":19940274,"hhi_median":110090,"hhi_renter_median":72142,"std_rti":0.5855},
{"name":"Los Angeles, CA","population":12927614,"hhi_median":103873,"hhi_renter_median":75879,"std_rti":0.4191},
{"name":"Chicago, IL","population":9406924,"hhi_median":99645,"hhi_renter_median":61308,"std_rti":0.3924},
{"name":"Dallas-Fort Worth, TX","population":8344032,"hhi_median":100686,"hhi_renter_median":66870,"std_rti":0.2586},
{"name":"Houston, TX","population":7796182,"hhi_median":88043,"hhi_renter_median":57581,"std_rti":0.2813},
{"name":"Miami, FL","population":6457988,"hhi_median":88838,"hhi_renter_median":62069,"std_rti":0.4737},
{"name":"Washington, DC","population":6437907,"hhi_median":131504,"hhi_renter_median":85342,"std_rti":0.2953},
{"name":"Atlanta, GA","population":6409047,"hhi_median":99391,"hhi_renter_median":63164,"std_rti":0.2988},
{"name":"Philadelphia, PA","population":6330422,"hhi_median":97580,"hhi_renter_median":58646,"std_rti":0.3734},
{"name":"Phoenix, AZ","population":5186958,"hhi_median":97859,"hhi_renter_median":68403,"std_rti":0.2667},
{"name":"Boston, MA","population":5025517,"hhi_median":124803,"hhi_renter_median":74832,"std_rti":0.4631},
{"name":"Riverside, CA","population":4744214,"hhi_median":96758,"hhi_renter_median":66091,"std_rti":0.4134},
{"name":"San Francisco, CA","population":4648486,"hhi_median":150999,"hhi_renter_median":106164,"std_rti":0.3218},
{"name":"Detroit, MI","population":4400578,"hhi_median":80496,"hhi_renter_median":47200,"std_rti":0.3508},
{"name":"Seattle, WA","population":4145494,"hhi_median":121441,"hhi_renter_median":84938,"std_rti":0.2854},
{"name":"Minneapolis-St. Paul, MN","population":3757952,"hhi_median":103154,"hhi_renter_median":60336,"std_rti":0.3101},
{"name":"Tampa, FL","population":3424560,"hhi_median":83724,"hhi_renter_median":58257,"std_rti":0.3564},
{"name":"San Diego, CA","population":3298799,"hhi_median":116460,"hhi_renter_median":83254,"std_rti":0.4000},
{"name":"Denver, CO","population":3050512,"hhi_median":117176,"hhi_renter_median":76449,"std_rti":0.2712},
{"name":"Orlando, FL","population":2940513,"hhi_median":87352,"hhi_renter_median":62377,"std_rti":0.3286},
{"name":"Charlotte, NC","population":2883370,"hhi_median":93550,"hhi_renter_median":61971,"std_rti":0.2934},
{"name":"Baltimore, MD","population":2859024,"hhi_median":104831,"hhi_renter_median":62321,"std_rti":0.3266},
{"name":"St. Louis, MO","population":2809527,"hhi_median":87170,"hhi_renter_median":50166,"std_rti":0.3205},
{"name":"San Antonio, TX","population":2763006,"hhi_median":83471,"hhi_renter_median":54067,"std_rti":0.2723},
{"name":"Austin, TX","population":2550637,"hhi_median":113668,"hhi_renter_median":77956,"std_rti":0.2149},
]
def band(r):
if r < 0.30: return "affordable"
if r < 0.40: return "moderate burden"
return "severe burden"
results = []
for d in data:
renter_rti = d["std_rti"] * (d["hhi_median"]/d["hhi_renter_median"])
gap_pp = (renter_rti - d["std_rti"])*100
income_discount = (1 - d["hhi_renter_median"]/d["hhi_median"])*100
results.append({
"name": d["name"],
"population": d["population"],
"std_rti_pct": d["std_rti"]*100,
"renter_rti_pct": renter_rti*100,
"gap_pp": gap_pp,
"renter_income_discount_pct": income_discount,
"std_band": band(d["std_rti"]),
"renter_band": band(renter_rti),
"band_changed": band(d["std_rti"]) != band(renter_rti),
})
results.sort(key=lambda x: -x["gap_pp"])
for r in results:
flag = " <== BAND SHIFT" if r["band_changed"] else ""
print(f"{r['name']:26s} std={r['std_rti_pct']:5.1f}% renter={r['renter_rti_pct']:5.1f}% gap=+{r['gap_pp']:5.1f}pp renter-income-discount={r['renter_income_discount_pct']:5.1f}% [{r['std_band']} -> {r['renter_band']}]{flag}")
avg_std = sum(r['std_rti_pct'] for r in results)/len(results)
avg_renter = sum(r['renter_rti_pct'] for r in results)/len(results)
print(f"\nAvg across top 25 markets: std={avg_std:.1f}% renter={avg_renter:.1f}% (+{avg_renter-avg_std:.1f}pp)")
with open('/vercel/sandbox/work/results2.json','w') as f:
json.dump(results, f, indent=2)
EOF
python3 /vercel/sandbox/work/rti2.pycat /vercel/sandbox/work/results2.json | python3 -c "
import json,sys
r=json.load(sys.stdin)
flips_from_affordable = [x for x in r if x['std_band']=='affordable' and x['renter_band']=='severe burden']
print('Affordable -> Severe burden flips:')
for x in flips_from_affordable:
print(' ', x['name'], x['std_rti_pct'], '->', x['renter_rti_pct'])
"
The standard rent-to-income ratio is lying to you about seven of the country's biggest rental markets — Atlanta, DC, Charlotte, Houston, San Antonio, Denver, and Seattle all read as "affordable" (under 30%) on the published metric but are actually in severe cost-burden territory (over 40%) once you use what renters actually earn.
The mechanism is simple: overall median household income blends homeowners — who earn far more and are the majority in every one of these markets — with renters. Across the top 25 markets, renter median income runs 27–43% below the overall median . That gap is what makes the standard ratio look better than reality almost everywhere.
| Market | Population | Standard RTI % | Renter-Adjusted RTI % | Gap (pp) | Renter Income Discount vs. Overall Median (%) | Affordability Band (Standard) | Affordability Band (Renter-Adjusted) | Band Changed? |
|---|---|---|---|---|---|---|---|---|
| 5,025,517 | 46.31% | 77.23%2 | 30.92 | 40.04% | Severe1 | Severe1 | No change | |
| 19,940,274 | 58.55% | 89.35%2 | 30.80 | 34.47% | Severe1 | Severe1 | No change | |
| 6,330,422 | 37.34% | 62.10%2 | 24.76 | 39.90% | Moderate1 | Severe1 | Changed | |
| 4,400,578 | 35.08% | 59.83%2 | 24.75 | 41.36% | Moderate1 | Severe1 | Changed | |
| 9,406,924 | 39.24% | 63.78%2 | 24.54 | 38.47% | Moderate1 | Severe1 | Changed | |
| 2,809,527 | 32.05% | 55.69%2 | 23.64 | 42.46% | Moderate1 | Severe1 | Changed | |
| 2,859,024 | 32.66% | 54.94%2 | 22.28 | 40.55% | Moderate1 | Severe1 | Changed | |
| 3,757,952 | 31.01% | 53.02%2 | 22.01 | 41.52% | Moderate1 | Severe1 | Changed | |
| 6,457,988 | 47.37% | 67.80%2 | 20.43 | 30.13% | Severe1 | Severe1 | No change | |
| 4,744,214 | 41.34% | 60.52%2 | 19.18 | 31.70% | Severe1 | Severe1 | No change | |
| 6,409,047 | 29.88% | 47.01%2 | 17.13 | 36.44% | Affordable1 | Severe1 | Changed | |
| 6,437,907 | 29.53% | 45.50%2 | 15.97 | 35.10% | Affordable1 | Severe1 | Changed | |
| 3,298,799 | 40.00% | 55.95%2 | 15.95 | 28.51% | Moderate1 | Severe1 | Changed | |
| 3,424,560 | 35.64% | 51.22%2 | 15.58 | 30.42% | Moderate1 | Severe1 | Changed | |
| 12,927,614 | 41.91% | 57.37%2 | 15.46 | 26.96% | Severe1 | Severe1 | No change | |
| 2,883,370 | 29.34% | 44.29%2 | 14.95 | 33.75% | Affordable1 | Severe1 | Changed | |
| 7,796,182 | 28.13% | 43.01%2 | 14.88 | 34.60% | Affordable1 | Severe1 | Changed | |
| 2,763,006 | 27.23% | 42.04%2 | 14.81 | 35.23% | Affordable1 | Severe1 | Changed | |
| 3,050,512 | 27.12% | 41.57%2 | 14.45 | 34.75% | Affordable1 | Severe1 | Changed | |
| 4,648,486 | 32.18% | 45.76%2 | 13.58 | 29.69% | Moderate1 | Severe1 | Changed | |
| 2,940,513 | 32.86% | 46.02%2 | 13.16 | 28.60% | Moderate1 | Severe1 | Changed | |
| 8,344,032 | 25.86% | 38.94%2 | 13.08 | 33.58% | Affordable1 | Moderate1 | Changed | |
| 4,145,494 | 28.54% | 40.81%2 | 12.27 | 30.06% | Affordable1 | Severe1 | Changed | |
| 5,186,958 | 26.67% | 38.14%2 | 11.47 | 30.11% | Affordable1 | Moderate1 | Changed | |
| 2,550,637 | 21.49% | 31.34%2 | 9.85 | 31.41% | Affordable1 | Moderate1 | Changed |
The pattern by market size:
The average across all 25 markets moves from 34.3% (standard) to 52.5% (renter-adjusted) — an 18-point gap that turns a "moderate burden" national picture into a "severe burden" one . If you're underwriting rent growth, screening a market for rent-paying capacity, or sizing what a renter base can absorb, the renter-median denominator is the one to use — the overall-median version is systematically flattering because it's answering a different question (what could the median household pay) than the one that matters for a rental asset (what can the median renter pay).
This is a read-only snapshot of a RealAI analysis.