Spaces:
Running
Running
| """ | |
| scripts/prepare_v12_features.py | |
| ================================ | |
| Enriches features_v5.csv with 7 new Overture POI categories β features_v6.csv | |
| New feature columns (500m count): | |
| poi_atm_500m, poi_urgent_care_500m, poi_cinema_500m, | |
| poi_library_500m, poi_childcare_500m, poi_beauty_500m, poi_hotel_500m | |
| Uses pre-extracted CSV files from process_nyc_overture_qol.py | |
| (much faster than re-reading 425K overture_places.geojson). | |
| Run: | |
| cd /Users/totam/Desktop/new_try | |
| python scripts/prepare_v12_features.py | |
| """ | |
| import numpy as np | |
| import polars as pl | |
| import pandas as pd | |
| from pathlib import Path | |
| from sklearn.neighbors import BallTree | |
| BASE = Path(__file__).resolve().parent.parent | |
| RAW = BASE / "data" / "raw" | |
| PROC = BASE / "data" / "processed" | |
| INPUT = PROC / "features_v5.csv" | |
| OUTPUT = PROC / "features_v6.csv" | |
| RADIUS_M = 500 | |
| RADIUS_RAD = RADIUS_M / 6_371_000 # Earth radius in metres | |
| # New POI categories: (column_name, csv_file) | |
| NEW_POIS = [ | |
| ("atm", RAW / "nyc_overture_atms.csv"), | |
| ("urgent_care", RAW / "nyc_overture_urgent_care.csv"), | |
| ("cinema", RAW / "nyc_overture_cinemas.csv"), | |
| ("library", RAW / "nyc_overture_libraries.csv"), | |
| ("childcare", RAW / "nyc_overture_childcare.csv"), | |
| ("beauty", RAW / "nyc_overture_beauty_salons.csv"), | |
| ("hotel", RAW / "nyc_overture_hotels.csv"), | |
| ] | |
| def count_within_radius(poi_lats: np.ndarray, poi_lons: np.ndarray, | |
| query_lats: np.ndarray, query_lons: np.ndarray) -> np.ndarray: | |
| """Return count of POIs within RADIUS_M for each query point.""" | |
| if len(poi_lats) == 0: | |
| return np.zeros(len(query_lats), dtype=np.int32) | |
| poi_rad = np.radians(np.column_stack([poi_lats, poi_lons])) | |
| qry_rad = np.radians(np.column_stack([query_lats, query_lons])) | |
| tree = BallTree(poi_rad, metric="haversine") | |
| counts = tree.query_radius(qry_rad, r=RADIUS_RAD, count_only=True) | |
| return counts.astype(np.int32) | |
| print(f"Loading {INPUT.name} β¦") | |
| df = pl.read_csv(INPUT) | |
| print(f" {len(df):,} rows Γ {len(df.columns)} cols") | |
| lats = df["latitude"].to_numpy() | |
| lons = df["longitude"].to_numpy() | |
| new_series = [] | |
| for col_name, csv_path in NEW_POIS: | |
| feat_col = f"poi_{col_name}_500m" | |
| # Skip if already present | |
| if feat_col in df.columns: | |
| print(f" {feat_col}: already present β skip") | |
| continue | |
| if not csv_path.exists(): | |
| print(f" {feat_col}: {csv_path.name} not found β zeros") | |
| new_series.append(pl.Series(feat_col, np.zeros(len(df), dtype=np.int32))) | |
| continue | |
| poi_df = pd.read_csv(csv_path) | |
| poi_lats = poi_df["lat"].to_numpy(dtype=float) | |
| poi_lons = poi_df["lon"].to_numpy(dtype=float) | |
| # Drop NaN coordinates | |
| mask = ~(np.isnan(poi_lats) | np.isnan(poi_lons)) | |
| poi_lats, poi_lons = poi_lats[mask], poi_lons[mask] | |
| counts = count_within_radius(poi_lats, poi_lons, lats, lons) | |
| new_series.append(pl.Series(feat_col, counts)) | |
| print(f" {feat_col}: {len(poi_lats):,} POIs | " | |
| f"median={np.median(counts):.0f} max={counts.max()}") | |
| df = df.with_columns(new_series) | |
| print(f"\nSaving β {OUTPUT.name}") | |
| df.write_csv(OUTPUT) | |
| print(f" Shape: {len(df):,} rows Γ {len(df.columns)} cols") | |
| # Sanity check | |
| print("\nNew column stats:") | |
| for col_name, _ in NEW_POIS: | |
| feat_col = f"poi_{col_name}_500m" | |
| if feat_col in df.columns: | |
| arr = df[feat_col].to_numpy() | |
| print(f" {feat_col:30s} median={np.median(arr):.1f} " | |
| f"p90={np.percentile(arr, 90):.0f} max={arr.max()}") | |