Colin Sidberry

Home Search

A real estate data visualization tool for finding underpriced neighborhoods.

The problem

Living in the Bay Area is great. Most of the time. Great weather, great people, great food...

But it's also expensive AF, especially when you're trying to buy a house. The old adage of "buy the worst house in the best neighborhood" doesn't really hold up here. You can easily spend a million dollars and still end up with a fixer-upper.

So I built a tool to give myself better insight into market fundamentals so I can estimate the underlying value of a neighborhood and actually be equipped to find deals in a very crowded market.

See it in action

The whole thing is one map. Each neighborhood sits on a single color spectrum, red for overpriced and green for underpriced, so the deals jump out at a glance. Zoom from the entire Bay Area down to a single block, and toggle the data layers behind the score.

The Bay Area: every scored metro at a glance
San José: 29 zips, colored by upside
Block level: the individual dots

The data layers

Transit: VTA light rail, Caltrain & BART
Jobs: employers sized by headcount
For sale: listings flagged against sold comps

How the score works

It starts with raw data about each neighborhood: new building permits going up nearby, what comparable homes recently sold for, job postings in the area, school ratings, crime reports, transit access, and more.

The catch: these numbers are in completely different units (dollars, counts, test scores, miles), so you can't compare them directly. The fix is a z-score: each value is restated as how many standard deviations it sits above or below the metro average, which puts everything on one scale.

z=x − μσ
  • x: the zip's raw value for one indicator
  • μ: the average across all zips in that metro
  • σ: the standard deviation across that metro

Scoring is per-metro, so a zip is only compared to its own city. Adding a new metro never shifts another's scores.

The score is just a weighted blend of those component z-scores. The sliders in the app change the weights wₖ and recompute the blend live in the browser:

Score=wₖ·zₖ

Growth potential goes one step further: it takes the blend of forward-looking fundamentals and subtracts the current price z-score, so it shows where the upside hasn't been paid for yet.

That final score is what colors each zip on the map:

← overvalued / less upsideunderpriced for the upside →

Architecture

Dozens of raw source tables feed a single derived view, zip_analysis, which does all the aggregation and scoring. The map only ever reads the derived view.

Raw data
prices
rents
schools
permits
crime
jobs
amenities
listings
transit
growth
Analysis
zip_analysis
z-scored & blended per metro · one column per indicator
View
Map
re-weights client-side

The view stores each indicator's z-score in its own column, not just the final blend, so the browser already has every component it needs. Moving a slider re-blends numbers that are already loaded instead of going back to the database. And every source row behind those numbers is browsable in the database viewer.

Technologies

Frontend
Next.js
Next.js
React
React
Tailwind
Tailwind
MapLibre
MapLibre
Data
PostgreSQL
PostgreSQL
Neon
Neon
PostGIS
PostGIS
Data pipeline
Python
Python
Playwright
Playwright
Hosting
Vercel
Vercel