How does the tenancy of Aspen Court Apartments, Plano TX compare to its comp set and the wider submarket? What does this tell you about leasing upside and any delinquency risks?
import math, statistics as st
# Subject
S = dict(name="Aspen Court Apartments", lat=33.022528588771905, lon=-96.76471889019012,
units=70, yb=1985, yr=None, style="GARDEN", sf=785,
ipr=1646.11, ask=1550.00, psf=2.13, occ=0.9714, ret=0.9286, dom=69,
fico=693.26, pd=0.0250, cc75=0.23, hhi=70223, nw=1.853125, wr=2.1375,
mob=12.9307, adults=67, bal=11480.62, rti=0.2785)
# Candidate pool: Plano market-rate rentals, garden/low-rise/townhouse, pre-2000 vintage,
# with property-level tenant credit data (fico non-null)
C = [
("The Fairway",33.023955523967835,-96.77065193653107,256,1979,2005,"GARDEN",847,1286.97,1328.97,1.55,0.9531,0.6133,120,657.98,0.0970,0.29,63345,1.407859,1.915989,15.59,229,9313.25,0.2444),
("Creekside Village",33.02536636590966,-96.75643622875214,480,1983,2014,"GARDEN",857,1501.32,1455.68,1.79,0.8479,0.6896,147,586.86,0.2381,0.39,60127,1.200407,1.512716,12.28,294,6927.57,0.3009),
("The Leighton Apartments",33.0237516760827,-96.75193011760713,208,1978,2017,"LOW_RISE",753,1258.50,1378.89,1.67,0.9423,0.6923,114,612.14,0.1572,0.35,72164,1.610959,1.682192,10.81,108,10619.81,0.2035),
("The Lydian Apartments",33.03813368082056,-96.75160825252534,160,1976,1980,"GARDEN",809,1390.42,1508.43,1.71,0.9813,0.6875,136,637.63,0.1115,0.30,86725,0.507895,1.357895,20.98,115,12717.84,0.1860),
("MAA Highwood",33.03016215562829,-96.79080069065094,196,1983,None,"GARDEN",797,1319.69,1309.14,1.68,0.9541,0.6480,82,686.67,0.1125,0.25,82558,3.261084,2.977011,13.96,167,18375.03,0.1850),
("The Westside",33.02152544260034,-96.73062264919281,412,1984,2014,"GARDEN",862,1338.83,1474.46,1.60,0.8689,0.2451,74,679.00,0.0960,0.24,105266,2.174387,1.762943,10.59,281,8974.50,0.1501),
("The Hathaway at Willow Bend",33.032318651676256,-96.79673373699188,219,1984,2006,"GARDEN",1022,1445.05,1381.43,1.41,0.9543,0.6393,32,644.46,0.1464,0.29,51709,1.774834,1.880795,11.49,177,13326.71,0.3342),
("Preserve at Preston",33.033230602741334,-96.79109036922455,380,1984,2017,"LOW_RISE",795,1306.13,1379.04,1.64,0.8921,0.5132,30,633.09,0.1590,0.34,86731,1.878333,2.005000,10.76,190,13437.15,0.1720),
("Old Shepard Place",33.02055984735498,-96.79339706897736,244,1989,2016,"LOW_RISE",739,1337.93,1282.00,1.81,0.9508,0.6844,110,663.09,0.2012,0.26,72778,2.713841,2.706065,10.66,184,13496.95,0.2091),
("The Wexley Apartments",33.054720461368646,-96.75176918506624,204,1983,2016,"GARDEN",1007,1577.79,1563.53,1.57,0.9608,0.7353,77,635.70,0.1604,0.32,77305,1.383446,2.625000,13.86,207,13746.05,0.2504),
("Chisholm Place",33.02791446447381,-96.7152589559555,142,1981,None,"LOW_RISE",1150,1557.54,1626.00,1.36,0.9648,0.7113,75,625.41,0.1078,0.37,124246,2.239468,2.578714,12.11,129,15598.67,0.1473),
("The Dayton",33.05501550436028,-96.78730309009552,389,1985,None,"LOW_RISE",827,1309.39,1255.27,1.59,0.8560,0.3522,88,654.28,0.1843,0.28,87343,2.181907,2.437743,13.63,306,12031.40,0.1690),
("Cottages at Tulane",33.03440004587182,-96.79331123828888,268,1990,2011,"GARDEN",730,1299.92,1487.73,1.80,0.9515,0.7761,69,644.72,0.1589,0.31,85123,1.423873,1.969549,10.37,225,13806.47,0.1649),
("Custer Park Apartments",33.042344748973946,-96.72992527484894,232,1978,2018,"GARDEN",958,1363.50,1327.27,1.45,0.9440,0.8017,84,650.43,0.0787,0.35,58055,0.321888,1.148069,11.69,161,10725.72,0.2821),
("Hunters Glen",33.059065639972765,-96.74800336360931,276,1979,2011,"GARDEN",946,1278.37,1201.25,1.39,0.8370,0.6667,97,647.61,0.1726,0.30,76737,0.673657,1.327654,13.51,192,12677.42,0.1836),
("Bellevue at Spring Creek",33.058641850948426,-96.74208104610445,278,1982,2001,"GARDEN",951,1489.49,1428.36,1.59,0.9568,0.7338,82,665.98,0.1028,0.28,93969,2.306527,2.102564,12.92,227,10280.01,0.1852),
("Legacy Apartments",33.070674240589234,-96.73896968364716,244,1984,2000,"GARDEN",879,1453.68,1458.08,1.66,0.9303,0.7500,98,647.98,0.0919,0.30,88647,2.351572,2.755530,14.53,222,13001.85,0.1898),
("Fox Trails Apartments",33.058808147907335,-96.74498856067657,286,1981,2014,"GARDEN",961,1440.99,1480.85,1.54,0.8776,0.2692,76,661.62,0.1193,0.31,99288,2.254072,2.421281,11.64,267,11337.06,0.1638),
("Waterford on the Meadow",33.01689594984063,-96.66307389736176,350,1985,2003,"GARDEN",888,1539.37,1428.26,1.76,0.9029,0.6600,85,578.82,0.2530,0.42,99110,1.313253,1.679217,15.32,196,5225.55,0.1846),
("Villas at Chase Oaks",33.06726247072229,-96.702384352684,250,1986,2022,"GARDEN",735,1339.32,1521.61,1.73,0.9760,0.9120,59,662.26,0.0850,0.24,81449,1.580645,1.529777,9.72,219,15136.32,0.1852),
]
cols = ["name","lat","lon","units","yb","yr","style","sf","ipr","ask","psf","occ","ret","dom",
"fico","pd","cc75","hhi","nw","wr","mob","adults","bal","rti"]
pool=[dict(zip(cols,r)) for r in C]
def miles(a,b):
R=3958.8; p1,p2=math.radians(a['lat']),math.radians(b['lat'])
dp=p2-p1; dl=math.radians(b['lon']-a['lon'])
return 2*R*math.asin(math.sqrt(math.sin(dp/2)**2+math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2))
for c in pool: c['mi']=miles(S,c)
pool.sort(key=lambda c:c['mi'])
comps=pool[:10]
print("COMP SET (10 closest 1976-1990 garden/low-rise Plano market-rate with tenant credit data)")
print(f"{'Property':32s}{'mi':>5s}{'Un':>5s}{'Yr':>6s}{'Reno':>6s}{'$/u':>8s}{'$/sf':>6s}{'Occ':>7s}{'Ret':>7s}{'FICO':>6s}{'PastDue':>8s}{'CC75':>6s}{'HHI':>8s}")
for c in comps:
print(f"{c['name']:32s}{c['mi']:5.1f}{c['units']:5d}{c['yb']:6d}{str(c['yr']):>6s}{c['ipr']:8.0f}{c['psf']:6.2f}{c['occ']*100:6.1f}%{c['ret']*100:6.1f}%{c['fico']:6.0f}{c['pd']*100:7.1f}%{c['cc75']*100:5.0f}%{c['hhi']:8,.0f}")
print(f"{'SUBJECT: Aspen Court':32s}{0:5.1f}{S['units']:5d}{S['yb']:6d}{'None':>6s}{S['ipr']:8.0f}{S['psf']:6.2f}{S['occ']*100:6.1f}%{S['ret']*100:6.1f}%{S['fico']:6.0f}{S['pd']*100:7.1f}%{S['cc75']*100:5.0f}%{S['hhi']:8,.0f}")
def med(k): return st.median([c[k] for c in comps])
print("\n--- SUBJECT vs COMP-SET MEDIAN vs WEST PLANO SUBMARKET ---")
sub = dict(ipr=1611.61, psf=1.76, occ=0.9233, ret=0.652, dom=69, fico=750.99, pd=0.0425,
cc75=0.16, hhi=154355, bal=23439.77, tradeout=-0.0324, ipr_t12=-0.0078)
rows=[("In-place rent/unit","ipr","$%.0f"),("In-place rent/sf","psf","$%.2f"),
("Occupancy","occ","%.1f%%"),("Retention (T12)","ret","%.1f%%"),
("Avg tenant FICO","fico","%.0f"),("Past-due rate","pd","%.1f%%"),
("CC accts >=75% util","cc75","%.1f%%"),("Median HH income","hhi","$%,.0f"),
("Non-mtg debt balance","bal","$%,.0f"),("Rent-to-income","rti","%.1f%%")]
for label,k,fmt in rows:
sv=S.get(k); cv=med(k) if k in comps[0] else None; bv=sub.get(k)
def f(v):
if v is None: return "n/a"
if k in ("occ","ret","pd","cc75","rti"): return f"{v*100:.1f}%"
if k in ("hhi","bal"): return f"${v:,.0f}"
if k=="fico": return f"{v:.0f}"
if k=="psf": return f"${v:.2f}"
return f"${v:,.0f}"
delta = "" if (sv is None or cv is None) else f" vs comps: {((sv-cv)/cv*100):+.1f}%"
print(f"{label:24s} subj {f(sv):>10s} | comp med {f(cv):>10s} | submkt {f(bv):>10s}{delta}")
# Loss to lease / leasing upside
ltl = S['ask']/S['ipr']-1
comp_ltl = st.median([c['ask']/c['ipr']-1 for c in comps])
print(f"\nSubject asking vs in-place (loss-to-lease): {ltl*100:+.1f}% (ask ${S['ask']:.0f} vs in-place ${S['ipr']:.0f})")
print(f"Comp-set median asking vs in-place: {comp_ltl*100:+.1f}%")
print(f"Subject asking cut last 3 months: $1,690 -> $1,550 = {(1550/1690-1)*100:+.1f}%")
print(f"Submarket new-lease tradeout (T30 avg): {sub['tradeout']*100:+.1f}%; submarket in-place rent T12: {sub['ipr_t12']*100:+.1f}%")
# per-sf premium vs comps and submarket
print(f"\nSubject $/sf premium vs comp median: {(S['psf']/med('psf')-1)*100:+.1f}%; vs submarket avg: {(S['psf']/sub['psf']-1)*100:+.1f}%")
print(f"Subject unit size {S['sf']} sf vs comp median {med('sf'):.0f} sf")
# delinquency-risk composite: share of comps with worse past-due
worse = sum(1 for c in comps if c['pd']>S['pd'])
print(f"\nComps with higher past-due rate than subject: {worse}/{len(comps)}")
print(f"Comp-set past-due range: {min(c['pd'] for c in comps)*100:.1f}% - {max(c['pd'] for c in comps)*100:.1f}%")
print(f"Comp-set FICO range: {min(c['fico'] for c in comps):.0f} - {max(c['fico'] for c in comps):.0f}")
print(f"Subject credit sample: {S['adults']} adults tagged, FICO n=6 (97% CI 663-723) -> directional only")
# implied rent if pushed to comp psf
print(f"\nIf subject held comp-median $/sf (${med('psf'):.2f}) on 785sf -> ${med('psf')*785:.0f}/mo vs actual ${S['ipr']:.0f}")
cd /vercel/sandbox && python comp_set_tenancy.py
The tenant base is the best credit in its comp set — but the rent has already been pushed past what that base supports, and the asking rent was cut 8% in the last three months to prove it.
Aspen Court's residents carry a 693 average FICO against a 645 comp-set median, and a 2.5% past-due rate against 15.2% — every one of the ten nearest 1970s–80s garden comps in Plano has worse delinquency . That quality is showing up in operations: 92.9% retention versus 66.6% for the comp set and 65.2% submarket-wide , 97.1% occupancy against a 92.3% submarket average . This is a stable, low-churn rent roll.
| Metric | Comp-set median | Read | ||
|---|---|---|---|---|
| Avg tenant FICO | 6931 | 645 | 751 | Strongest in comp set, below broader submarket |
| Past-due rate | 2.5%1 | 15.2% | 4.2% | Far cleanest in comp set |
| Credit cards ≥75% utilized | 23%1 | 29.5% | 16% | Better than comps, worse than submarket |
| Median tenant household income | $70,223 | $77,668 | $154,3552 | Below comps |
| Rent-to-income | 27.9% | 19.5% | n/a | Most stretched in the set |
| In-place rent per sf | $2.13 | $1.67 | $1.76 | 27% above comps |
| Retention T12 | 92.9% | 66.6% | 65.2% | Far above |
| Occupancy | 97.1% | 95.1% | 92.3% | Above |
Where the picture disagrees with itself. The credit file says prime; the spending and the rent math say stretched. The top borrowing behavior at Aspen Court is living paycheck to paycheck, against having a mortgage for ZIP 75075 generally , and the resident base skews to "upcoming retirees" with an average household head age of 61 — a fixed- or fixing-income cohort. Rent-to-income sits at 27.9%, roughly 8 points above the comp median and near the 30% cost-burden line , and tenant income here is 10% below the comp set even as rent per square foot runs 27% above it . Clean credit in this building reflects older, debt-light households ($11.5k non-mortgage balances vs. $13.0k for comps ), not spare capacity.
Leasing upside is already spent. Asking rent is $1,550 against $1,646 in place — negative 5.8% loss-to-lease, where the comp set still carries +4.4% — and asking has been cut from $1,690 three months ago, down 8.3% . Units took 69 days to lease in a submarket where new-lease tradeouts are running −3.2% . At $2.13/sf on 785-sf units, the property is priced 21% above the West Plano average per foot with no renovation on record since 1985 construction .
What to do with it. Renewals are the whole game: at 92.9% retention and negative loss-to-lease, every unit that turns re-leases down, so the value here is protecting the renewal base with modest 2–3% bumps rather than chasing the $1,646 in-place number on new leases. Second, the thing to underwrite is affordability, not credit — a fixed-income tenancy at 27.9% rent-to-income absorbs a small increase and breaks on a large one, so a value-add thesis premised on pushing rents toward a renovated-comp level would trade this retention for churn at a lower achievable rent. Third, the $136,990/unit basis from the June 2025 sale already capitalizes today's above-market rent; there is no mark-to-market gap left to buy.
Delinquency risk reads low and I'd underwrite it that way — with the caveat that the credit read rests on six scored adults, so treat 693 as a band (663–723), not a number . The rent data covers 19 of 70 units . If you have a rent roll and T12, I can tie the retention and bad-debt picture to actual unit-level delinquency and size the renewal scenarios in a workbook.
This is a read-only snapshot of a RealAI analysis.