Analyze occupancy, rent, and retention trends across 4000 Hulen, Rienzi at Turtle Creek, and Lakes At Lewisville in the Dallas/Fort Worth market. My current focus is benchmarking against direct rent comps, and I'm particularly concerned whether any are lagging peers. Break down what's driving the performance differences between these three assets and flag where operational or leasing gaps need to be addressed.
Got the three IDs. Now let's pull the full operating picture for each.
Now pull core data for the three properties in parallel.
mkdir -p /vercel/sandbox/analysis && cat > /vercel/sandbox/analysis/dfw_compare.py << 'EOF'
import json
props = {
"4000 Hulen": {
"submarket": "Ridgelea (Fort Worth)",
"unit_count": 240, "year_built": 2015, "style": "Mid-rise",
"occ_latest": 0.9125, "occ_3mo": 0.9125, "occ_6mo": 1.0000, "occ_12mo": 0.9333,
"sm_occ_latest": 0.8168, "sm_occ_12mo": 0.9282,
"retention": 0.9083, "sm_retention": 0.658,
"tradeout_pct": 0.0546, "sm_tradeout_pct": 0.0134,
"dom": None, "sm_dom": 75,
"asking_1bed": 1371.20, "inplace_1bed": 1223.94, "sm_asking_1bed": 1368.51, "sm_inplace_1bed": 1533.84,
"asking_2bed": 1767.29, "inplace_2bed": 1636.23, "sm_asking_2bed": 1918.54, "sm_inplace_2bed": 2040.84,
"rent_sqft": 1.63, "sm_rent_sqft": 2.04,
"noi_pct_egi": 0.4344, "opex_pct_egi": 0.5656, "tax_pct_egi": 0.2282, "payroll_pct_egi": 0.1244, "mgmt_fee_pct_egi": 0.0300,
"egi": 4135882.67, "vacancy_loss": 377301.96,
"fico": None, "past_due": None,
},
"Rienzi at Turtle Creek": {
"submarket": "South Oak Lawn (Uptown Dallas)",
"unit_count": 152, "year_built": 2002, "year_renovated": 2020, "style": "High-rise",
"occ_latest": 0.9539, "occ_3mo": 0.9539, "occ_6mo": 0.9605, "occ_12mo": 0.9079,
"sm_occ_latest": 0.9304, "sm_occ_12mo": 0.9599,
"retention": 0.7632, "sm_retention": 0.684,
"tradeout_pct": -0.1587, "sm_tradeout_pct": 0.0318,
"dom": 243, "sm_dom": 83,
"asking_1bed": 3091.00, "inplace_1bed": 2522.44, "sm_asking_1bed": 2202.39, "sm_inplace_1bed": 1897.57,
"asking_2bed": 3629.89, "inplace_2bed": 3269.68, "sm_asking_2bed": 3202.97, "sm_inplace_2bed": 2684.10,
"rent_sqft": 2.45, "sm_rent_sqft": 2.38,
"noi_pct_egi": None, "opex_pct_egi": None, "tax_pct_egi": None, "payroll_pct_egi": None, "mgmt_fee_pct_egi": None,
"egi": None, "vacancy_loss": 268350.54,
"fico": 743.76, "past_due": 0.0456,
},
"Lakes At Lewisville": {
"submarket": "Coppell/South Lewisville",
"unit_count": 214, "year_built": 2000, "year_renovated": 2022, "style": "Low-rise",
"occ_latest": 0.9439, "occ_3mo": 0.9486, "occ_6mo": 0.9439, "occ_12mo": 0.9626,
"sm_occ_latest": 0.9378, "sm_occ_12mo": 0.9318,
"retention": 0.6308, "sm_retention": 0.671,
"tradeout_pct": -0.0312, "sm_tradeout_pct": -0.0198,
"dom": 179, "sm_dom": 70,
"asking_1bed": 1184.00, "inplace_1bed": 1233.23, "sm_asking_1bed": 1235.68, "sm_inplace_1bed": 1259.30,
"asking_2bed": 1563.64, "inplace_2bed": 1550.17, "sm_asking_2bed": 1580.69, "sm_inplace_2bed": 1545.74,
"rent_sqft": 1.69, "sm_rent_sqft": 1.71,
"noi_pct_egi": 0.5464, "opex_pct_egi": 0.4536, "tax_pct_egi": 0.1392, "payroll_pct_egi": 0.0895, "mgmt_fee_pct_egi": 0.0300,
"egi": 3900864.06, "vacancy_loss": 214741.55,
"fico": 621.73, "past_due": 0.1667,
},
}
print(f"{'Metric':38}{'4000 Hulen':>16}{'Rienzi':>16}{'Lakes Lewisville':>18}")
def row(label, key, pct=False, mult=1):
vals = []
for p in props.values():
v = p.get(key)
if v is None:
vals.append("n/a")
elif pct:
vals.append(f"{v*100*mult:.1f}%")
else:
vals.append(f"{v:,.2f}" if isinstance(v,float) else str(v))
print(f"{label:38}{vals[0]:>16}{vals[1]:>16}{vals[2]:>18}")
row("Occupancy (latest)", "occ_latest", pct=True)
row("Occupancy (12mo ago)", "occ_12mo", pct=True)
row("Submarket occ (latest)", "sm_occ_latest", pct=True)
row("Submarket occ (12mo ago)", "sm_occ_12mo", pct=True)
print()
row("Retention rate (T12)", "retention", pct=True)
row("Submarket retention", "sm_retention", pct=True)
print()
row("New-lease tradeout %", "tradeout_pct", pct=True)
row("Submarket tradeout %", "sm_tradeout_pct", pct=True)
print()
row("Days on market (signed L30D)", "dom")
row("Submarket DOM", "sm_dom")
print()
row("1BR asking rent", "asking_1bed")
row("Submarket 1BR asking", "sm_asking_1bed")
row("1BR in-place rent", "inplace_1bed")
row("Submarket 1BR in-place", "sm_inplace_1bed")
print()
row("2BR asking rent", "asking_2bed")
row("Submarket 2BR asking", "sm_asking_2bed")
row("2BR in-place rent", "inplace_2bed")
row("Submarket 2BR in-place", "sm_inplace_2bed")
print()
row("In-place rent/sqft", "rent_sqft")
row("Submarket rent/sqft", "sm_rent_sqft")
print()
row("NOI margin (% EGI)", "noi_pct_egi", pct=True)
row("OpEx ratio (% EGI)", "opex_pct_egi", pct=True)
row("Property tax (% EGI)", "tax_pct_egi", pct=True)
row("Payroll (% EGI)", "payroll_pct_egi", pct=True)
row("Mgmt fee (% EGI)", "mgmt_fee_pct_egi", pct=True)
print()
row("Avg tenant FICO", "fico")
row("Past-due rate", "past_due", pct=True)
print("\n--- Derived calcs ---")
# occupancy delta net of submarket move
for name,p in props.items():
occ_delta_vs_sm = (p["occ_latest"] - p["sm_occ_latest"])*100
occ_chg_12mo_prop = (p["occ_latest"] - p["occ_12mo"])*100
occ_chg_12mo_sm = (p["sm_occ_latest"] - p["sm_occ_12mo"])*100
manager_attrib_occ = occ_chg_12mo_prop - occ_chg_12mo_sm
print(f"{name}: occ vs submarket = {occ_delta_vs_sm:+.1f}pp | property 12mo chg = {occ_chg_12mo_prop:+.1f}pp | submarket 12mo chg = {occ_chg_12mo_sm:+.1f}pp | manager-attributable 12mo delta = {manager_attrib_occ:+.1f}pp")
print()
# retention gap dollar impact using retention_proj_annual_unit_turnovers analog: use unit_count*(1-retention)
turnover_cost_per_unit = 2200 # blended assumption, disclose
for name,p in props.items():
turns_actual = p["unit_count"] * (1-p["retention"])
turns_at_sm_retention = p["unit_count"] * (1-p["sm_retention"])
excess_turns = turns_actual - turns_at_sm_retention
cost = excess_turns * turnover_cost_per_unit
print(f"{name}: actual turns/yr={turns_actual:.0f}, turns/yr at submarket retention={turns_at_sm_retention:.0f}, excess turns={excess_turns:+.1f}, est. annual cost @ ${turnover_cost_per_unit}/turn = ${cost:,.0f}")
print()
# rent/sqft gap vs submarket - $ per unit per month using avg unit sqft
sqft = {"4000 Hulen":881, "Rienzi at Turtle Creek":1259, "Lakes At Lewisville":879}
for name,p in props.items():
gap_sqft = p["rent_sqft"] - p["sm_rent_sqft"]
gap_dollar_unit = gap_sqft * sqft[name]
print(f"{name}: rent/sqft gap vs submarket = {gap_sqft:+.2f} => ${gap_dollar_unit:+.0f}/unit/mo at avg {sqft[name]} sqft")
print()
# vacancy loss as % of GPR
gpr = {"4000 Hulen":4099417.20, "Rienzi at Turtle Creek":5606836.88, "Lakes At Lewisville":3724527.00}
vac = {"4000 Hulen":377301.96, "Rienzi at Turtle Creek":268350.54, "Lakes At Lewisville":214741.55}
for name in props:
print(f"{name}: vacancy loss / GPR = {vac[name]/gpr[name]*100:.1f}%")
EOF
python3 /vercel/sandbox/analysis/dfw_compare.pyDFW three-asset comparison: Lakes at Lewisville is the laggard — weak retention, slow leasing, and a credit-stressed renter base are compounding. Rienzi is winning on occupancy but bleeding on trade-outs. 4000 Hulen is out-executing a submarket in freefall and sitting on unclaimed rent.
| Property | Occupancy (latest) | Occupancy 12mo ago | Submarket occupancy (latest) | Retention rate (T12) | Submarket retention | New-lease tradeout % | Submarket tradeout % | Days on market (signed, past 30d) | Submarket DOM | In-place rent/sqft | Submarket rent/sqft | Avg tenant FICO | Past-due rate |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 91.3% | 93.3% | 81.7%1 | 90.8% | 65.8%1 | +5.5% | +1.3%1 | n/a | 751 | $1.63 | $2.041 | n/a | n/a | |
| 95.4% | 90.8% | 93.0% | 76.3% | 68.4% | -15.9% | +3.2% | 243 | 83 | $2.45 | $2.38 | 744 | 4.6% | |
| 94.4% | 96.3% | 93.8% | 63.1% | 67.1% | -3.1% | -2.0% | 179 | 70 | $1.69 | $1.71 | 622 | 16.7% |
Lakes At Lewisville is the one that needs intervention, not just a comp check. Its 63.1% retention is the weakest of the three and actually trails its own submarket average (67.1%) — the only asset here losing tenants faster than the market around it. Days-on-market for recently signed leases is 179, 2.6x the submarket's 70 , and new leases are trading out negative (-3.1%) in a submarket that's itself soft (-2.0%) but not this soft. Layer in the tenant base: average FICO of 622 sits right at the subprime line, and 16.7% of tenant credit accounts are past due — nearly 4x Rienzi's 4.6% . That's not a market story; submarket occupancy actually rose 0.6pp over the trailing year while Lakes' own occupancy fell 1.9pp — a manager-attributable erosion of roughly 2.5pp net of the market move . The read: a credit-stressed resident base is driving turnover, and leasing isn't re-filling units fast enough once they turn — slow absorption plus a discounting posture on new leases. This is where the operational gap actually sits.
Rienzi's paradox: occupancy is winning, but the mechanism paying for it should worry you. Occupancy climbed from 90.8% to 95.4% over the year while its submarket (Uptown/Oak Lawn) fell from 96.0% to 93.0% — a genuine ~7.5pp manager-attributable gain . But new leases are trading out at -15.9% against prior tenant rent, while the submarket itself trades out positive (+3.2%) , and days-on-market for signed leases is 243 — nearly 3x the submarket's 83. Rienzi is buying its occupancy win by discounting hard at turn in a submarket where comps are actually pushing rent. With a wealthy, high-FICO (744) resident base that should be sticky at a fair renewal, a 76.3% retention rate (only modestly above the 68.4% submarket average) suggests renewals aren't being priced or timed well — tenants are leaving who shouldn't be, and the occupancy is getting backfilled at a discount rather than defended at the lease. That's a leasing/pricing execution gap, not a demand problem — the demand signal (submarket rent momentum) says this asset should be trading out positive, not negative.
4000 Hulen is the standout on execution, and it's leaving rent on the table because of it. Its submarket (Ridgelea) is in genuine freefall — occupancy cratered from 92.8% to 81.7% in a year, an 11.1pp collapse, almost certainly new-supply pressure given the pace. 4000 Hulen's own occupancy dropped only 2.1pp in the same window — a 9.1pp manager-attributable outperformance, the widest gap of the three — and retention of 90.8% is nearly 25pp above the submarket's 65.8% . That's a property holding its tenant base through a submarket that's losing residents to newer competition. The cost of that discipline: in-place rent runs $0.41/sqft below the submarket ($1.63 vs. $2.04), a ~$361/unit/month gap at the property's 881 sqft average — but asking rent is priced at parity with the submarket ($1,371 vs. $1,369 on a 1BR), so the gap is concentrated in the existing lease book, not the marketing price. With retention this strong, renewals aren't being pushed toward market — that's the classic tension: the same discipline that's protecting occupancy is also suppressing loss-to-lease capture. Separately, its 22.8% property-tax load (vs. Lakes' 13.9%) is compressing NOI margin to 43.4% — that's an assessment/jurisdiction fact, not a leasing issue, worth a tax-appeal look but not a manager mark.
What's driving the split: Lakes' problem is tenant quality plus leasing velocity — a subprime-adjacent renter base cycling out faster than the team is filling behind them. Rienzi's problem is pricing discipline at renewal — good demand, undisciplined execution at the lease event. 4000 Hulen's "problem" is the good kind — a resilient, retention-driven asset with unrealized rent sitting in the existing lease book.
Next 30 days: Push a renewal-pricing audit at Rienzi — with 744 FICO and a submarket trading out positive, that -15.9% tradeout should not exist. At Lakes, tighten the leasing funnel (179 days is the number to watch weekly) and get eyes on the delinquency trend before it becomes a bad-debt problem. At 4000 Hulen, start pricing renewals toward the $2.04/sqft submarket mark on the next wave of expirations — the retention cushion can absorb it.
This is a read-only snapshot of a RealAI analysis.