thaman / docs /DATA_CATALOG.md
Turki Almurahhem
THAMAN dual-city AVM β€” Stack v22 (NYC) + v12 (Riyadh)
eb7e023
|
Raw
History Blame Contribute Delete
33.5 kB
# THAMAN β€” Data Catalog
> AI-Powered PropTech System for Property Valuation
> Last updated: March 2026
---
## Riyadh Data Sources
| # | Dataset | File | Records | Notes |
|---|---|---|---|---|
| R1 | Real Estate Transactions (2018–2023) | `data/raw/quarter_report SI.xlsx` | ~6,500 rows | Saudi Open Data baseline; district-level quarterly aggregates |
| R2 | RE Transactions 2024 Q1 | `data/raw/` (Saudi Open Data CSV) | ~300 rows | Property transactions by type + district |
| R3 | RE Transactions 2024 Q3 | `data/raw/` (Saudi Open Data CSV) | ~300 rows | |
| R4 | RE Transactions 2024 Q4 | `data/raw/` (Saudi Open Data CSV) | ~300 rows | |
| R5 | RE Transactions 2025 Q1–Q3 | `data/raw/` (Saudi Open Data CSVs) | ~600 rows | Holdout period |
| R6 | Metro Stations | `data/raw/metro-stations-in-riyadh-*.geojson` | 85 stations | 6 lines; opened 2024 |
| R7 | Bus Stops | `data/raw/bus-stops-in-riyadh-*.geojson` | ~2,500 stops | Includes BRT stops |
| R8 | Traffic Intersections | `data/raw/traffic-intersections-*.geojson` | ~8,000 intersections | Major street crossings |
| R9 | Commercial Services | `data/raw/commercial-services-*.geojson` | ~12,000 POIs | 10 categories (hypermarkets, banks, restaurants…) |
| R10 | Air Quality Stations | `data/raw/air-quality-stations-*.geojson` | 12 stations | NOβ‚‚, SOβ‚‚, PM₁₀, O₃ measurements |
| R11 | Air Quality Readings | `data/raw/air-quality.csv` | ~500 rows | Station-level pollutant means |
| R12 | Mosques | `data/raw/riyadh_mosques.csv` | ~3,000 | OSM-derived |
| R13 | Malls | `data/raw/riyadh_malls.csv` | ~80 | OSM-derived |
| R14 | Schools | `data/raw/riyadh_schools.csv` | ~600 | OSM-derived |
| R15 | Hospitals | `data/raw/riyadh_hospitals.csv` | ~120 | OSM-derived |
| R16 | Parks | `data/raw/riyadh_parks.csv` | ~200 | OSM-derived |
| R17 | Entertainment Venues | `data/raw/rcrc_entertainment.csv` | ~150 | RCRC leisure venues |
| R18 | Rental Listings (SA_Aqar) | `data/raw/SA_Aqar.csv` | 960 listings | District-level medians: size, bedrooms, age, rent/sqm |
| R19 | Real Estate Price Index | `data/raw/real-estate-indices.csv` | ~200 rows | REI residential + apartment quarterly (2019–2025) |
| R20 | District Polygons | `data/processed/riyadh_district_polygons.geojson` | 133 polygons | OSM Overpass admin_level=10; 107/133 enriched |
| R21 | District Centroids | `data/processed/district_centroids.csv` | 147 centroids | Derived from OSM polygon centroids |
**Processed output:** `data/processed/features_riyadh.csv` β€” 6,910 rows Γ— 87 columns
**Pipeline:** `scripts/riyadh_feature_engineering.py` (Polars-native)
---
## Changelog
| Date | File | Change |
|---|---|---|
| 2026-03-08 | `feature_engineering.py` | Initial pipeline created β€” 26 columns: property sales, NTA assignment, KD-tree distances (subway, school, park, hospital), BallTree POI count, crime/noise rates, building age |
| 2026-03-08 | `data/raw/overture_places.geojson` | Replaced broken OSM POI export (12,886 records) with Overture Maps via leafmap (425,387 records) |
| 2026-03-08 | `data/raw/nta_boundaries.geojson` | Added `population_2020` (Census 2020 Decennial) and `median_income_nta` (ACS 5-year 2020) enriched into NTA file via census tract spatial join |
| 2026-03-08 | `data/raw/mta_bus_stops.csv` | Added bus stop locations from MTA GTFS S3 feeds (4 boroughs, 9,747 stops); added `dist_bus_m` feature |
| 2026-03-08 | `feature_engineering.py` | Added grouped imputation for `gross_square_feet`, `land_square_feet`, `residential_units` (35.8% null β†’ 0% null) using median by `bldgclass` + `borough` |
| 2026-03-08 | `feature_engineering.py` | Added `renovated_since_2018` and `years_since_renovation` from DOB A1/A2 permits via NYC Open Data |
| 2026-03-08 | `feature_engineering.py` | Added `mortgage_rate_30yr` from FRED MORTGAGE30US; joined to sales via `merge_asof` on sale date |
| 2026-03-08 | `feature_engineering.py` | Added `is_landmark` and `is_historic_district` from LPC dataset (endpoint: ncre-qhxs); matched via BBL |
| 2026-03-08 | `feature_engineering.py` | Added `dist_waterfront_m` from OSM Overpass coastline query (33,507 points) via KD-tree |
| 2026-03-08 | `feature_engineering.py` | Added `dist_bike_lane_m` from OSM Overpass cycleway query (4,155 segments) via KD-tree |
| 2026-03-08 | `feature_engineering.py` | Added `dist_elem_school_m` from NYC elementary school directory (423 schools) via KD-tree |
| 2026-03-08 | `feature_engineering.py` | Added `dist_express_subway_m` and `nearest_station_is_express` from MTA GTFS Daytime Routes column (380 express stations) |
| 2026-03-08 | `feature_engineering.py` | Added `livability_complaint_rate` from 311 heat/rodent/dirty complaints (383,778 records); normalized per 1k NTA residents |
| 2026-03-08 | `feature_engineering.py` | Added `borough_income_deviation` (NTA median income minus borough median income) |
| 2026-03-08 | `feature_engineering.py` | Added `sale_year` and `sale_month` for temporal/seasonality signals |
| 2026-03-08 | `DATA_CATALOG.md` | Added raw data source entries #14–20 (DOB permits, FRED, elementary schools, 311 livability, LPC landmarks, OSM coastline, OSM bike lanes) |
| 2026-03-08 | `DATA_CATALOG.md` | Expanded "What Could Further Improve the Model" from 7 β†’ 15 items with difficulty/notes columns |
| 2026-03-08 | `data/processed/features.csv` | Added FAR/zoning fields from PLUTO: `residfar`, `commfar`, `facilfar`, `builtfar`, `maxallwfar`, `far_utilization` (40 β†’ 46 cols). Fix: actual column is `commfar` not `comfar`; `maxallwfar` computed as max(residfar, commfar, facilfar) |
| 2026-03-08 | `data/processed/features.csv` | Added building type flags from `bldgclass`: `has_elevator`, `is_condo`, `is_multifamily`, `is_single_fam`, `is_mixed_use` (46 β†’ 51 cols). Fix: fill NaN bldgclass before `.str.startswith()` |
| 2026-03-08 | `data/raw/airbnb_listings.csv` | Downloaded Inside Airbnb NYC December 2025 snapshot (36,261 listings). URL required `.csv.gz` format + browser User-Agent header; earlier dated snapshot URLs returned 403 |
| 2026-03-08 | `data/processed/features.csv` | Added `airbnb_count_500m` via BallTree β€” 92.9% of properties have β‰₯1 Airbnb within 500m; median=18. corr with sale_price: +0.078 (51 β†’ 52 cols) |
| 2026-03-08 | `data/processed/features.csv` | Added ACRIS prior sale features: `prior_sale_price`, `prior_sale_date`, `price_appreciation`, `years_since_prior_sale`, `is_flip` (52 β†’ 57 cols). Source: ACRIS Master (bnx9-e6tj) + Legals (8h5j-fqxa) joined on document_id. 50 concurrent batches by borough+block in 31s. Match rate: 13.7%; prior_sale_price corr with sale_price: +0.719 |
| 2026-03-08 | `data/processed/features.csv` | Added school district quality composite: `school_district`, `district_avg_score`, `district_school_count` (57 β†’ 60 cols). District boundaries from ArcGIS REST (NYC Open Data GeoJSON endpoint returned 400). 272 HS schools across 19 of 33 districts; 56.9% null (districts without HS data) |
| 2026-03-09 | `models/xgboost_model.json` | XGBoost trained: 917 trees, RΒ²=0.735, MedAPE=17.8%, MAE=$726,593 on test set. Ridge baseline RΒ²=0.238. Pre-processing: log1p target, winsorize 3 cols, label-encode bldgclass, 80/20 split by borough |
| 2026-03-09 | `models/scorer.py` | ThamanScorer class created β€” loads model, encodes inputs, returns predicted price + confidence range Β±17.8% |
| 2026-03-09 | `models/shap_importance.png` | SHAP top-20 chart: gross_sqft #1, bldgclass #2, land_sqft #3, longitude #4, latitude #5, school_district #6 |
| 2026-03-09 | `models/actual_vs_predicted.png` | Actual vs Predicted scatter (properties < $10M, RΒ²=0.735) |
| 2026-03-09 | `models/error_by_borough.png` | % error by borough boxplot |
| 2026-03-08 | `data/processed/features.csv` | Full audit + 6 data quality fixes: (1) building_age capped at 200 — 984 outliers fixed; (2) years_since_prior_sale clipped to 0 — 542 negatives fixed; (3) added `has_prior_sale` binary flag + corrected `is_flip` logic; (4) district_avg_score imputed with borough→global median — 0% null; (5) numfloors imputed by bldgclass+borough — 0% null; (6) far_utilization nulls filled with 0. Final: 36,203 × 61 cols, all non-ACRIS cols 0% null |
| 2026-03-09 | `api/main.py` | FastAPI backend created β€” `POST /predict` returns predicted price + SHAP top-10 drivers; `POST /batch` handles up to 50 properties; CORS enabled for frontend |
| 2026-03-09 | `api/spatial.py` | SpatialLookup class β€” loads 7 spatial datasets at startup; KD-tree for transit/school/park distances; BallTree for Airbnb density; NTA point-in-polygon for crime/income/school-district features |
| 2026-03-09 | `api/models.py` | Pydantic schemas β€” PredictRequest (8 required, 20+ optional fields), PredictResponse (price, range, SHAP drivers, spatial summary) |
| 2026-03-09 | `models/scorer.py` | Bug fix: ACRIS defaults changed from `0.0` to `np.nan` in `predict_single` β€” training median imputation now correctly applies (was causing ~100Γ— underpricing) |
| 2026-03-09 | `frontend/index.html` | Map UI created β€” Leaflet.js NYC map + property form + result/SHAP/spatial panels served at `/ui` |
| 2026-03-09 | `frontend/style.css` | Full UI stylesheet β€” CSS variables, 2-col layout, animated cards, SHAP bars, responsive at 768px |
| 2026-03-09 | `frontend/app.js` | Browser JS β€” map click, borough auto-detect, form validation, fetch `/predict`, SHAP bar + spatial grid render |
| 2026-03-09 | `api/main.py` | Added `StaticFiles` mount at `/ui` β†’ `frontend/`; root `/` now serves `index.html` directly; API info moved to `/api` |
| 2026-03-09 | `data/processed/features.csv` | Expanded dataset 5Γ— β€” from 36,203 rows (2025 only) to 185,092 rows (2022–2026) via `scripts/download_more_sales.py`. Added 1 new column: `assesstot` (PLUTO total assessed value) for prior_sale_price imputation (62 cols total) |
| 2026-03-09 | `data/processed/features.csv` | Prior sale coverage improved from 13.7% β†’ ~99% using `assesstot` ratios to estimate prior_sale_price for properties without ACRIS history |
| 2026-03-09 | `models/xgboost_model.json` | Retrained XGBoost base learner on 185K rows with Spatial GroupKFold CV (5 folds by NTA) |
| 2026-03-09 | `models/thaman_stack.pkl` | Trained stacking ensemble v2.1: XGBoost + LightGBM + CatBoost base learners + Ridge meta-learner. RΒ²=0.6509, MedAPE=20.29% on spatial holdout |
| 2026-03-09 | `models/meta.json` | Updated to 71 feature names (added log-dist + target-encoded cols), stack metrics, `bldgclass_means`, `borough_bldg_means`, `walk_score_scaler` |
| 2026-03-09 | `api/main.py` | Fixed 500 error on `/predict` β€” `int(NaN)` on `school_district` spatial lookup. Added `_safe_int()` and `_safe_round()` helpers. Updated metrics to v2.1 |
| 2026-03-10 | `tests/conftest.py` | Created module-scoped pytest fixture β€” `with TestClient(app) as c` triggers FastAPI lifespan (model + spatial data load) for test suite |
| 2026-03-10 | `tests/test_api.py` | Fixed 17 test functions to use `client` fixture; renamed `test_root_redirects` β†’ `test_root_serves_ui` (root now returns 200, not redirect) |
| 2026-03-10 | `tests/test_scorer.py` | Fixed hardcoded feature counts: 70 β†’ 71 (2 assertions) |
---
## Feature Matrix (Model Input)
**File:** `data/processed/features.csv`
**Rows:** 185,092 properties | **Columns:** 62
**Target variable:** `sale_price` (log-transform required β€” raw skewness: 54.9)
**Sale date range:** 2022 – 2026 (NYC Rolling Sales, 4 years)
| Column | Type | Null % | Notes |
|---|---|---|---|
| `sale_price` | float | 0% | Target. Filter: > $10,000. Log-transform before training |
| `building_age` | int | 0% | Current year minus `yearbuilt` |
| `numfloors` | float | 2.9% | From PLUTO |
| `bldgclass` | str | 0% | NYC building class code (D4=elevator apt, A1=single family, etc.) |
| `gross_square_feet` | float | 35.8% | High null rate in Manhattan (condos) β€” impute with median by bldgclass |
| `land_square_feet` | float | 35.8% | Same |
| `residential_units` | float | 35.8% | Same |
| `dist_subway_m` | float | 0% | KD-tree distance to nearest subway station |
| `dist_bus_m` | float | 0% | KD-tree distance to nearest bus stop |
| `dist_express_subway_m` | float | 0% | Distance to nearest express subway station |
| `nearest_station_is_express` | int (0/1) | 0% | 1 if nearest subway station is an express stop |
| `dist_elem_school_m` | float | 0% | Distance to nearest elementary school |
| `dist_waterfront_m` | float | 0% | KD-tree distance to NYC coastline (OSM) |
| `dist_bike_lane_m` | float | 0% | Distance to nearest bike lane/cycleway |
| `is_landmark` | int (0/1) | 0% | 1 if property is an NYC Landmark (LPC) |
| `is_historic_district` | int (0/1) | 0% | 1 if in an LPC Historic District |
| `renovated_since_2018` | int (0/1) | 0% | 1 if DOB A1/A2 permit issued since 2018 |
| `years_since_renovation` | int | 0% | Years since last major permit (999 = never) |
| `mortgage_rate_30yr` | float | 0% | 30yr fixed mortgage rate at time of sale (FRED) |
| `sale_year` | int | 0% | Year of sale |
| `sale_month` | int | 0% | Month of sale (1–12, captures seasonality) |
| `livability_complaint_rate` | float | 0% | Heat/rodent/dirty complaints per 1k NTA residents |
| `borough_income_deviation` | float | 0% | NTA median income minus borough median income |
| `dist_school_m` | float | 0% | KD-tree distance to nearest school |
| `dist_park_m` | float | 0% | KD-tree distance to nearest park centroid |
| `dist_hospital_m` | float | 0% | KD-tree distance to nearest health POI (Overture) |
| `poi_count_500m` | int | 0% | BallTree count of Overture places within 500m |
| `crime_rate_nta` | float | 0% | Crimes per 1,000 residents in NTA (2022–2024) |
| `noise_density_nta` | float | 0% | Noise complaints per 1,000 residents in NTA (2022–2024) |
| `median_income_nta` | float | 0% | Median household income in NTA (ACS 2020) |
| `population_2020` | int | 0% | NTA population from 2020 Census |
| `residfar` | float | 0% | Residential Floor Area Ratio allowed by zoning (PLUTO) |
| `commfar` | float | 0% | Commercial FAR allowed by zoning (PLUTO) |
| `facilfar` | float | 0% | Facility FAR allowed by zoning (PLUTO) |
| `builtfar` | float | 0% | Actual built FAR (PLUTO) |
| `maxallwfar` | float | 0% | Max allowed FAR = max(residfar, commfar, facilfar) |
| `far_utilization` | float | 0.3% | builtfar / maxallwfar β€” how fully the lot is developed (capped at 5) |
| `has_elevator` | int (0/1) | 0% | 1 if building class is elevator-type (D class, select R class) |
| `is_condo` | int (0/1) | 0% | 1 if bldgclass starts with R (condo/co-op) |
| `is_multifamily` | int (0/1) | 0% | 1 if bldgclass starts with D (multifamily elevator) |
| `is_single_fam` | int (0/1) | 0% | 1 if bldgclass starts with A (single family) |
| `is_mixed_use` | int (0/1) | 0% | 1 if bldgclass starts with S (mixed use) |
| `airbnb_count_500m` | int | 0% | Airbnb listings within 500m (Inside Airbnb Dec 2025); median=18, corr=+0.078 |
| `prior_sale_price` | float | 86.3% | Price of second most recent DEED for same BBL (ACRIS); corr with sale_price: +0.719 |
| `prior_sale_date` | datetime | 86.3% | Date of prior sale |
| `price_appreciation` | float | 86.3% | (sale_price - prior_sale_price) / prior_sale_price; clipped to [-1, 10] |
| `years_since_prior_sale` | float | 86.3% | Years between prior deed and current sale |
| `is_flip` | int (0/1) | 0% | 1 if prior sale exists AND years_since_prior_sale < 2 |
| `has_prior_sale` | int (0/1) | 0% | 1 if ACRIS prior deed found for this BBL (13.7% coverage) |
| `school_district` | int | 0.03% | NYC school district number (1–32) |
| `district_avg_score` | float | 56.9% | Average graduation rate of HS schools in the district |
| `district_school_count` | int | 56.9% | Number of high schools in the district |
---
## Raw Data Sources
### 1. Property Sales
**File:** `data/raw/sales_geocoded.csv`
**Records:** 185,092 filtered (price > $10k) across 2022–2026 | originally 81,305 (2025 only)
**Description:** NYC property sale transactions across all 5 boroughs. Includes address, sale price, sale date, building class, and structural attributes joined from PLUTO via BBL. Expanded from 1 year (2025) to 4 years (2022–2026) using `scripts/download_more_sales.py`.
**Source:** [NYC Rolling Calendar Sales β€” NYC Open Data](https://www.nyc.gov/site/finance/property/property-rolling-sales-data.page)
---
### 2. PLUTO (Building Attributes)
**File:** `data/raw/nyc_pluto_25v4_csv/pluto_25v4.csv`
**Records:** 858,644 parcels | 92 columns
**Description:** Parcel-level building data for all NYC lots. Provides building age (yearbuilt), floor count, building class, zoning, lot area, and lat/lng coordinates used for geocoding the sales data.
**Source:** [NYC MapPLUTO β€” NYC Department of City Planning](https://www.nyc.gov/site/planning/data-maps/open-data/dwn-pluto-mappluto.page)
---
### 3. Overture Maps Places (POIs)
**File:** `data/raw/overture_places.geojson`
**Records:** 425,387 places
**Description:** Comprehensive point-of-interest dataset for NYC covering restaurants, shops, healthcare locations, services, and more. Used for `poi_count_500m` and `dist_hospital_m`. Replaced a broken OSM export that had only 12,886 records.
**Source:** [Overture Maps Foundation](https://overturemaps.org) via `leafmap.get_overture_data(type='place', bbox=NYC_BBOX)`
---
### 4. MTA Subway Stations
**File:** `data/raw/MTA_Subway_Stations_20260308.csv`
**Records:** 496 stations
**Description:** All NYC subway station locations with GTFS coordinates, line names, and ADA accessibility info. Used to compute `dist_subway_m`.
**Source:** [MTA Subway Stations β€” NYC Open Data](https://data.cityofnewyork.us/Transportation/Subway-Stations/arq3-7z49)
---
### 5. MTA Bus Stops
**File:** `data/raw/mta_bus_stops.csv`
**Records:** 9,747 stops (Manhattan + Brooklyn + Queens + Bronx)
**Description:** Bus stop locations from MTA GTFS feeds across all NYC boroughs. Used to compute `dist_bus_m` for the transit accessibility score.
**Source:** [MTA GTFS Static Feeds β€” rrgtfsfeeds.s3.amazonaws.com](https://rrgtfsfeeds.s3.amazonaws.com)
- Manhattan: `gtfs_m.zip`
- Brooklyn: `gtfs_b.zip`
- Queens: `gtfs_q.zip`
- Bronx: `gtfs_bx.zip`
---
### 6. Parks
**File:** `data/raw/parks_with_coords.csv`
**Records:** 2,058 parks (with centroids extracted from polygon geometry)
**Description:** NYC parks properties with WKT polygon geometry. Centroids were extracted using GeoPandas to compute `dist_park_m`.
**Source:** [Parks Properties β€” NYC Open Data](https://data.cityofnewyork.us/Recreation/Parks-Properties/enfh-gkve)
---
### 7. Schools
**File:** `data/raw/schools.csv`
**Records:** 427 schools
**Description:** NYC high school locations with quality metrics (graduation rate, attendance rate). Joined from two datasets: school directory (locations) and school quality report (metrics). Used for `dist_school_m`.
**Source (locations):** [NYC School Locations β€” NYC Open Data](https://data.cityofnewyork.us/resource/23z9-6uk9.csv)
**Source (quality):** [NYC School Quality Reports β€” NYC Open Data](https://data.cityofnewyork.us/resource/dnpx-dfnc.csv)
---
### 8. NYPD Crime Complaint Data
**File:** `data/raw/nypd_crimes.parquet`
**Records:** 1,646,571 complaints (2022–2024)
**Description:** All NYPD crime complaints with offense type, borough, date, and lat/lng. Used to compute `crime_rate_nta` (crimes per 1,000 NTA residents).
**Source:** [NYPD Complaint Data Historic β€” NYC Open Data](https://data.cityofnewyork.us/Public-Safety/NYPD-Complaint-Data-Historic/qgea-i56i)
---
### 9. NYC 311 Noise Complaints
**File:** `data/raw/noise_complaints.parquet`
**Records:** 1,000,000 complaints (2022–2024, filtered to Noise types)
**Description:** NYC 311 noise service requests with complaint type, date, and lat/lng. Types include Residential, Street/Sidewalk, Commercial, Vehicle. Used to compute `noise_density_nta`.
**Source:** [311 Service Requests β€” NYC Open Data](https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2010-to-Present/erm2-nwe9)
---
### 10. NTA Boundaries + Population + Income
**File:** `data/raw/nta_boundaries.geojson`
**Records:** 262 neighborhoods
**Description:** 2020 Neighborhood Tabulation Area boundaries enriched with:
- `population_2020` β€” from 2020 Decennial Census (P1_001N by tract, aggregated to NTA)
- `median_income_nta` β€” from ACS 5-year 2020 (B19013_001E by tract, aggregated to NTA)
Used to normalize crime and noise rates per 1,000 residents.
**Source (boundaries):** [NYC NTA 2020 β€” ArcGIS REST API](https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_Neighborhood_Tabulation_Areas_2020/FeatureServer/0)
**Source (population):** [US Census 2020 Decennial API](https://api.census.gov/data/2020/dec/pl)
**Source (income):** [US Census ACS 5-year 2020 API](https://api.census.gov/data/2020/acs/acs5)
---
### 11. Road Network
**Files:** `data/raw/road_network/*.graphml` (10 files)
**Records:** Drive + Walk networks for all 5 boroughs
**Description:** Full road and pedestrian network graphs downloaded via OSMnx from OpenStreetMap. Can be used for accurate network-distance routing (vs straight-line KD-tree approximation) and road type analysis.
| Borough | Drive nodes | Drive edges | Walk nodes | Walk edges |
|---|---|---|---|---|
| Manhattan | 4,628 | 9,916 | 36,631 | 116,868 |
| Brooklyn | 12,224 | 30,350 | 69,677 | 224,888 |
| Queens | 21,491 | 55,972 | 123,096 | 400,104 |
| Bronx | 7,723 | 19,259 | 39,392 | 120,888 |
| Staten Island | 9,209 | 23,464 | 59,469 | 169,418 |
**Source:** [OpenStreetMap](https://www.openstreetmap.org) via [OSMnx](https://osmnx.readthedocs.io)
---
### 12. Census Tract Population
**File:** `data/raw/census_tract_population.csv`
**Records:** 2,327 NYC tracts
**Description:** 2020 Decennial Census population (P1_001N) for every census tract in NYC's 5 counties. Used as an intermediate step to compute NTA-level population via spatial join.
**Source:** [US Census Bureau 2020 Decennial API](https://api.census.gov/data/2020/dec/pl)
---
### 13. Census Tract Income
**File:** `data/raw/census_tract_income.csv`
**Records:** 2,327 NYC tracts
**Description:** ACS 5-year 2020 median household income (B19013_001E) by census tract. Used as an intermediate step to compute NTA-level median income via spatial join.
**Source:** [US Census Bureau ACS 5-year 2020 API](https://api.census.gov/data/2020/acs/acs5)
---
### 14. DOB Renovation Permits
**File:** `data/raw/dob_permits.csv`
**Records:** 2,673 unique BBLs with A1/A2 permits since 2018
**Description:** NYC Department of Buildings alteration permits (job types A1/A2) filed since 2018. Used to compute `renovated_since_2018` (binary flag) and `years_since_renovation`. BBL constructed from borough + block + lot columns matching PLUTO format.
**Source:** [DOB Permit Issuance β€” NYC Open Data](https://data.cityofnewyork.us/Housing-Development/DOB-Permit-Issuance/ipu4-2q9a)
---
### 15. FRED Mortgage Rates
**File:** `data/raw/mortgage_rates.csv`
**Records:** ~1,500 weekly observations (1971–2026)
**Description:** Weekly 30-year fixed mortgage rate from the Federal Reserve Economic Data (FRED). Joined to each property sale via `merge_asof` on sale date. Used as `mortgage_rate_30yr` feature capturing macro credit conditions at time of sale.
**Source:** [FRED MORTGAGE30US β€” St. Louis Federal Reserve](https://fred.stlouisfed.org/series/MORTGAGE30US)
---
### 16. Elementary Schools
**File:** `data/raw/elementary_schools.csv`
**Records:** 423 elementary schools
**Description:** NYC public elementary school (K–5/PK) locations filtered from the NYC school directory. Used to compute `dist_elem_school_m` via KD-tree. Separate from the high school dataset to capture proximity to schools relevant to families with young children.
**Source:** [NYC School Locations β€” NYC Open Data](https://data.cityofnewyork.us/resource/23z9-6uk9.csv)
---
### 17. NYC 311 Livability Complaints
**File:** `data/raw/livability_complaints.parquet`
**Records:** 383,778 complaints (2022–2024)
**Description:** NYC 311 service requests filtered to livability issue types: heat/hot water, rodent, and dirty conditions. Aggregated to NTA level and normalized per 1,000 residents to compute `livability_complaint_rate`. Complements noise complaints as a separate QoL dimension.
**Source:** [311 Service Requests β€” NYC Open Data](https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2010-to-Present/erm2-nwe9)
---
### 18. LPC Landmarks
**File:** Used directly from Socrata API (not cached)
**Records:** 39,385 landmark records β†’ 3,695 unique landmark BBLs
**Description:** New York City Landmarks Preservation Commission (LPC) designated landmarks and historic district properties. Matched to sales via BBL to compute `is_landmark` and `is_historic_district`. Landmarks can affect both value (premium) and renovation flexibility.
**Source:** [LPC Individual Landmarks β€” NYC Open Data](https://data.cityofnewyork.us/Housing-Development/LPC-Individual-Landmarks/ncre-qhxs)
---
### 19. NYC Coastline (OSM Overpass)
**File:** Used in-memory during pipeline (not saved separately)
**Records:** 33,507 coastline points
**Description:** NYC shoreline geometry retrieved via OpenStreetMap Overpass API (`natural=coastline` within NYC bounding box). Used to compute `dist_waterfront_m` via KD-tree. Waterfront proximity is a major price driver in NYC, particularly for properties in Battery Park, DUMBO, Red Hook, and Williamsburg.
**Source:** [OpenStreetMap Overpass API](https://overpass-api.de) β€” query: `natural=coastline` within NYC bbox
---
### 20. Bike Lanes (OSM Overpass)
**File:** Used in-memory during pipeline (not saved separately)
**Records:** 4,155 bike lane segments β†’ coordinate points extracted
**Description:** NYC bike lane and cycleway geometries from OpenStreetMap via Overpass API. Segment coordinates extracted and used for KD-tree nearest-distance calculation (`dist_bike_lane_m`). Captures micro-mobility infrastructure as a livability and urban quality indicator.
**Source:** [OpenStreetMap Overpass API](https://overpass-api.de) β€” query: `highway=cycleway` + `cycleway` tags within NYC bbox
---
### 21. Inside Airbnb NYC Listings
**File:** `data/raw/airbnb_listings.csv`
**Records:** 36,261 active listings (December 2025 snapshot)
**Description:** Short-term rental listings across all 5 boroughs. Used to compute `airbnb_count_500m` β€” density of Airbnb units within 500m radius via BallTree. High Airbnb density correlates with tourist-heavy areas and mixed-use neighborhoods. Correlation with sale_price: +0.078.
**Source:** [Inside Airbnb β€” New York City](https://insideairbnb.com/get-the-data/) (free, no key). Note: use `.csv.gz` URL format + browser User-Agent header; plain CSV URLs return 403.
---
### 22. ACRIS Real Property Master
**File:** Used directly from Socrata API (not cached)
**Records:** 500,000 most recent DEED transactions (2011–2026)
**Description:** NYC property deed transfer records from the Automated City Register Information System. Contains `document_id`, sale date, and sale amount. **Does not contain BBL** β€” must be joined with ACRIS Legals (source #23) on `document_id` to obtain the BBL.
**Source:** [ACRIS Real Property Master β€” NYC Open Data](https://data.cityofnewyork.us/City-Government/ACRIS-Real-Property-Master/bnx9-e6tj)
---
### 23. ACRIS Real Property Legals
**File:** Used directly from Socrata API (not cached)
**Records:** 22.5M total; ~24k fetched via 50 batched borough+block queries
**Description:** Maps each ACRIS document_id to its parcel (borough, block, lot). Used in combination with ACRIS Master to build `prior_sale_price`, `price_appreciation`, `years_since_prior_sale`, and `is_flip`. Downloaded in 50 concurrent batches (300 blocks per batch) in ~31 seconds.
**Source:** [ACRIS Real Property Legals β€” NYC Open Data](https://data.cityofnewyork.us/City-Government/ACRIS-Real-Property-Legals/8h5j-fqxa)
---
### 24. NYC School Districts
**File:** Used in-memory during pipeline (not saved separately)
**Records:** 33 school districts
**Description:** NYC school district boundary polygons. Used for spatial join to assign each property to a district, then joined with school quality metrics to compute `district_avg_score` and `district_school_count`. Note: NYC Open Data GeoJSON export endpoint returned 400 β€” used ArcGIS REST API instead.
**Source:** [NYC School Districts β€” ArcGIS REST API](https://services5.arcgis.com/GfwWNkhOj9bNBqoJ/arcgis/rest/services/NYC_School_Districts/FeatureServer/0)
---
## Data Quality Notes
> **Last audit:** 2026-03-10 β€” All 24 raw sources present on disk βœ“ | Feature matrix: 185,092 Γ— 62 cols
| Issue | Detail | Status | Action Taken |
|---|---|---|---|
| Price skewness | Raw skewness = 54.86 | ⚠️ Requires transform | Apply `log1p(sale_price)` before training β€” log skewness = 0.78 βœ“ |
| gross_sqft nulls | 35.8% null (Manhattan condos) | βœ… Fixed | Imputed with median by `bldgclass` + `borough` β†’ 0% null |
| NTA outliers | BX1203 noise=2,711/1k, MN0502 crime=460/1k | ⚠️ Requires transform | Winsorize or log-transform at 99th percentile before training |
| Borough encoding | Stored as integers 1–5 | βœ… OK | Map: 1=Manhattan, 2=Bronx, 3=Brooklyn, 4=Queens, 5=Staten Island |
| building_age outlier | 984 properties with age > 200 (yearbuilt=0 in source) | βœ… Fixed | Capped at 200 years |
| years_since_prior_sale negatives | 542 properties with negative values (ACRIS ordering anomaly) | βœ… Fixed | Clipped to 0 minimum |
| is_flip ambiguity | Was 0 for no-prior-sale properties (misleading) | βœ… Fixed | Added `has_prior_sale` flag; is_flip only set 1 where prior sale exists |
| district_avg_score nulls | 56.9% null (districts with no HS data) | βœ… Fixed | Imputed with borough median β†’ global median fallback β†’ 0% null |
| numfloors nulls | 2.9% null | βœ… Fixed | Imputed with median by `bldgclass` + `borough` β†’ 0% null |
| far_utilization nulls | 0.3% null (vacant lots with no FAR) | βœ… Fixed | Filled with 0 (no development) |
| ACRIS prior sale coverage | 86.3% null β€” window limited to 2011–2026 | ⚠️ Known limitation | Use `has_prior_sale` as binary feature; model imputes missing with tree splits |
| NTA edge properties | 13 properties fell outside NTA boundary | βœ… Fixed | Filled with borough median for all NTA-level features |
| dist_bus_m outlier | Max 26,807m (Staten Island, no bus service) | βœ… Valid | Real data β€” Staten Island bus coverage is sparse. Keep as-is |
| dist_express_subway_m outlier | Max 27,592m (Staten Island β€” no express service) | βœ… Valid | Real data β€” Staten Island has no express subway. Keep as-is |
---
## Model Readiness Checklist
| Check | Status | Notes |
|---|---|---|
| All raw files on disk | βœ… 19/19 files present | Road network = 10 GraphML files |
| Feature matrix shape | βœ… 185,092 Γ— 62 cols | Expanded to 4 years of sales (2022–2026) |
| Non-ACRIS nulls | βœ… 0% null | All columns except ACRIS prior-sale group fully populated |
| ACRIS prior sale coverage | βœ… ~99% | `prior_sale_price` coverage improved using `assesstot` ratios for properties without ACRIS history |
| Target variable | βœ… Ready | Apply `log1p()` before training (skewness 54.86 β†’ 0.78) |
| Categorical encoding | βœ… Done | `bldgclass` β†’ target-encoded `bldgclass_encoded` + `borough_bldg_encoded` (cross-feature) |
| NTA outliers | βœ… Done | Winsorized at 99th percentile (`crime_rate_nta`, `noise_density_nta`, `livability_complaint_rate`) |
| Train/test split | βœ… Done | Spatial GroupKFold CV (5 folds by NTA) β€” prevents geographic leakage |
| Model | βœ… Done | Stack v2.1: XGB + LGB + CatBoost + Ridge meta | RΒ²=0.6509 | MedAPE=20.29% |
---
## What Could Further Improve the Model
| Data | Impact | Difficulty | Notes |
|---|---|---|---|
| Floor level (condo unit floor #) | Very High β€” strongest predictor for apartments | Very Hard | Not in public data; ACRIS deeds sometimes mention unit/floor but inconsistent |
| View / floor premium (condos) | High | Very Hard | Would require scraping StreetEasy/Zillow β€” not in public records |
| Interior sqft (condos) | High β€” 35.8% null in PLUTO for condos | Medium | ACRIS mortgage docs sometimes contain unit sizes; complex to parse |
| Elementary school quality ratings | High β€” distance added, quality not yet scored | Medium | NYC DOE K-8 quality reports; same long-format as HS dataset; join on `dbn` |
| ACRIS prior sale β€” expand window | Medium β€” only 13.7% matched | Easy | Add more 500k batches: `$offset=500000`, `$offset=1000000` to reach pre-2011 deeds |
| Air quality index by NTA | Medium β€” pollution negatively affects prices | Medium | EPA AQS API (free key at `aqs.epa.gov`); param 88101=PM2.5; IDW interpolation from monitors |
| Flood risk score | Medium β€” FEMA zones suppress coastal prices | Medium | FEMA NFHL API had SSL errors; download GDB locally from msc.fema.gov |
| Foreclosure / lis pendens rate | Medium β€” financial distress signal | Medium | ACRIS `doc_type='LIS PEN'`; same pipeline as sources #22–23 |
| Walk Score proxy | Medium β€” composite pedestrian signal | Easy | Compute from existing features: weighted(dist_subway, dist_bus, poi_count_500m) |
| Shadow / sunlight hours | Low–Medium | Hard | Requires NYC 3D Building Model + solar angle computation (pysolar) |