How the Investor Market Scorecard works
Every score, weight, data source, and limitation — documented. If you disagree with a methodology decision, you'll find the exact reasoning below. This page updates with every revision to the scoring logic.
Current data: 2026-04-30 · 894 metros scored · 8316 ZIPs scored
Contents
1. The Three Scores
Each market receives three scores on a 0–100 scale, where 100 = best in universe and 0 = worst in universe. Scores are computed as weighted percentile ranks across the universe of qualifying markets. They are not absolute measures — a score of 75 means "this market ranks in the 75th percentile for this dimension across all scored markets."
Cash Flow Score
Measures how well a market generates rental income relative to purchase price, weighted by evidence of active tenant demand. A high-yield market where homes sit unsold for 120+ days scores lower than a slightly lower-yield market where homes move in 20 days — because the latter is evidence that tenants and buyers are competing for the same space.
Components: Gross rent yield (60%) + Days to Pending inverse (40%).
Growth Score
Measures the appreciation trajectory of a market: where values have been and where Zillow's model expects them to go. A third leg — rent growth — cross-validates whether household demand supports the price trajectory.
Components: ZHVI 1-year change (40%) + ZHVF 12-month forecast (40%) + ZORI 1-year rent change (20%).
Stability Score
Measures how balanced and predictable a market is. An extremely hot market (homes selling in 5 days over ask) and an extremely cold market (homes sitting 200+ days) both score lower than a moderate market — because stability means predictable conditions, not maximum speed.
Components: Market Temperature Index (50%) + Absorption rate (25%) + Days to Pending bell curve (25%).
2. Components and Weights
Cash Flow Score — detail
| Component | Source | Weight | Notes |
|---|---|---|---|
| Gross rent yield | annual_rent / zhvi × 100 from mv_buy_vs_rent_ratio (metro) or fact_rentals + mv_zip_home_values (ZIP) |
60% | Percentile rank across universe. Null if no ZORI data. |
| Days to Pending (inverse) | mean_days_to_pending from mv_metro_market_snapshot |
40% | Lower DTP → higher score. Proxy for tenant demand and resale liquidity. |
Growth Score — detail
| Component | Source | Weight | Notes |
|---|---|---|---|
| ZHVI 1-year change | zhvi_yoy_pct from mv_metro_market_snapshot |
40% | Direct evidence of recent appreciation. |
| ZHVF 12-month forecast | zhvf_growth_rate from mv_metro_market_snapshot |
40% | Forward-looking signal from Zillow's forecast model. |
| ZORI 1-year change | zori_yoy_pct from mv_metro_market_snapshot |
20% | Rent growth confirms household demand. If unavailable, the other two components are re-weighted to 50/50. |
Stability Score — detail
| Component | Source | Weight | Notes |
|---|---|---|---|
| Market Temperature Index | market_temp_index from mv_metro_market_snapshot |
50% | Zillow's composite heat index — already aggregates multiple stability inputs. |
| Absorption rate | absorption_rate from mv_metro_market_snapshot |
25% | Higher absorption = healthier balance of supply and demand. |
| Days to Pending (bell curve) | mean_days_to_pending |
25% | Peaks at ~45 days. Markets too fast (<10) or too slow (>90) score lower than moderate-speed markets. Formula: MAX(0, 100 − |dtp − 45| × 1.5). |
3. The Math
Percentile rank
All components except the DTP bell curve use PostgreSQL window function percentile ranks:
pct_rank(c) = PERCENT_RANK() OVER (ORDER BY raw_value(c)) × 100
For "lower is better" components (Days to Pending in Cash Flow), the rank is inverted:
pct_rank_inv(c) = (1.0 − PERCENT_RANK() OVER (ORDER BY raw_value(c))) × 100
This means the market with the lowest DTP gets a percentile rank of ~100, and the market with the highest DTP gets ~0.
Missing-data re-weighting
When a component is null (e.g., no ZORI YoY data for a metro), the score is computed from the available components, with weights rescaled proportionally:
total_weight_present = SUM(weight(c) for c in components_present) score(d) = SUM(weight(c) × pct_rank(c) for c in components_present) / total_weight_present
Example: If ZORI YoY is null for a metro, the Growth score is computed from ZHVI YoY (40%) and ZHVF (40%), re-scaled to 50/50.
Qualification rules
A market is excluded from scoring entirely (not just penalized) if:
- Cash Flow: No ZORI data — cannot compute yield without rent
- Growth: All three components are null
- Stability: All three components are null
4. Signal Logic and Thresholds
Signals are derived on every query — they are not stored and are recomputed from scores each time. This means methodology revisions don't require database migrations.
| Signal | Conditions | Interpretation |
|---|---|---|
| Strong | All three scores ≥ 70 AND ZHVI 1-yr > 0 | Top-quartile across all three dimensions with positive appreciation. |
| Clear | All three scores ≥ 50 AND ZHVI 1-yr > 0 | Above median on all three dimensions with positive appreciation. No obvious red flags. |
| Watch | Cash Flow ≥ 75 AND exactly one of {Growth, Stability} < 40 | High yield, but one dimension is below average. Investigate the weak leg before committing. |
| High Risk | Cash Flow ≥ 75 AND (any score < 25 OR ZHVI 1-yr < −3%) | Yield trap warning: high yield paired with a serious weakness or active depreciation. |
| Avoid | Cash Flow ≥ 75 AND (≥ 2 scores < 25 OR ZHVI 1-yr < −5%) | Classic yield trap pattern: headline yield looks attractive but multiple dimensions signal severe risk. |
| Neutral | All other combinations | No dominant signal in either direction. Average market. |
| Unscored | Insufficient data | Market does not meet minimum data thresholds. See Coverage section. |
5. Coverage and Limitations
Metro coverage
The scorecard queries all MSAs in mv_metro_market_snapshot.
As of 2026-04-30:
- 894 total MSAs in the snapshot
- 894 MSAs receive full Cash Flow + Growth + Stability scoring — limited by ZORI YoY coverage
- The remaining MSAs appear with Stability and partial Growth scoring only, marked "Unscored" on Cash Flow
ZIP coverage
ZIP-level scoring covers only ZIPs with native ZORI rental data. As of the current release:
- 8316 ZIPs receive full scoring (approximately 29.6% of ~26,297 US ZIPs)
- ZIPs without ZORI data are not surfaced in the ZIP table — they are not scored and not shown as "Avoid" or "Unscored"
- Stability components (DTP, Market Temperature, Absorption) are inherited from the parent MSA at the ZIP level — they are not ZIP-native
What this tool does NOT measure
- No vacancy data. Days to Pending and Absorption Rate serve as demand proxies. Actual vacancy rates are not in the warehouse.
- No population or income trends. Census ACS data is deferred to a future version. ZHVI trend serves as the demand proxy in v1.
- No neighborhood-level signals. All scores are market-level or ZIP-level. Block or street-level analysis is not possible with this data.
- No cap rate calculation. Gross yield is not net yield. Operating expenses, vacancy allowance, property management, and financing costs are excluded.
- No property tax adjustment. High-tax states (e.g., IL, NJ) may show attractive gross yields that erode significantly after taxes.
- No crime or school data. Out of scope for v1.
6. Data Sources
| Dataset | Source | Refresh cadence | Warehouse table |
|---|---|---|---|
| Home values (ZHVI) | Zillow Research | Monthly | mv_metro_market_snapshot, mv_zip_home_values |
| Rental index (ZORI) | Zillow Research | Monthly | mv_metro_market_snapshot, fact_rentals |
| Home value forecast (ZHVF) | Zillow Research | Monthly | mv_metro_market_snapshot, fact_forecasts |
| Days to Pending | Zillow Research | Monthly | mv_metro_market_snapshot |
| Market Temperature Index | Zillow Research (composite) | Monthly | mv_metro_market_snapshot |
| Absorption Rate | Zillow Research | Monthly | mv_metro_market_snapshot |
| Buy vs. Rent Ratio | Computed from ZHVI + ZORI | Monthly (with source data) | mv_buy_vs_rent_ratio |
Refresh cadence
Scores recompute when materialized views refresh (currently monthly). The Redis cache
has a 7-day TTL — worst-case staleness after an MV refresh is 7 days. Cache keys embed
the latest observation_date, so a fresh MV refresh produces different cache
keys and naturally invalidates prior entries.
7. Changelog
Initial release. Metro-level scoring for all MSAs with ZORI coverage. ZIP-level scoring for ~7,780 ZIPs. Three composite dimensions: Cash Flow, Growth, Stability. Five signal tiers: Strong, Clear, Watch, High Risk, Avoid.