Spaces:
Sleeping
Sleeping
Download preprocess.py from DevKX/singapore-subzone-profiler: direct link, hf CLI and curl.
- Browser
- Download file 23.2 kB
-
https://huggingface.co/spaces/DevKX/singapore-subzone-profiler/resolve/main/preprocess.py
- Command line
-
hf download hf://spaces/DevKX/singapore-subzone-profiler/preprocess.py
-
curl -L -o preprocess.py https://huggingface.co/spaces/DevKX/singapore-subzone-profiler/resolve/main/preprocess.py
23.2 kB
| import argparse | |
| import json | |
| import os | |
| import sys | |
| import subprocess | |
| from typing import Dict, Tuple, Any, Optional | |
| import numpy as np | |
| import pandas as pd | |
| import joblib | |
| # ========================================== | |
| # 🛠️ RUNTIME DEPENDENCY INSTALLATION | |
| # ========================================== | |
| try: | |
| import geopandas as gpd | |
| except ImportError: | |
| print("📦 Installing geopandas dynamically...", flush=True) | |
| subprocess.check_call([ | |
| sys.executable, "-m", "pip", "install", "--quiet", | |
| "geopandas==0.10.2", "shapely<2.0.0", "fiona<1.9.0", "rtree" | |
| ]) | |
| import geopandas as gpd | |
| DEFAULT_INPUT_DIR = "raw_data" | |
| DEFAULT_OUTPUT_DIR = "processed_data" | |
| KEY_POI_THEMES = { | |
| "ssot_hawkercentres": "poi_hawker_count", | |
| "museum": "poi_museum_count", | |
| "votg_phpc": "poi_clinic_count", | |
| "libraries": "poi_library_count", | |
| "communityclubs": "poi_community_club_count", | |
| "monuments": "poi_monument_count", | |
| "historicsites": "poi_historic_site_count", | |
| "hotels": "poi_hotel_count", | |
| "tourism": "poi_attraction_count", | |
| "nationalparks": "poi_park_count", | |
| } | |
| def clean_subzone_series(series: pd.Series) -> pd.Series: | |
| """Normalizes subzone string codes/names for exact joining.""" | |
| return series.astype(str).str.strip().str.upper() | |
| def get_subzone_col(df: pd.DataFrame) -> Optional[str]: | |
| """Dynamically finds the subzone column name across varying raw CSV source schemas.""" | |
| possible_names = [ | |
| "SUBZONE_N", | |
| "SUBZONE", | |
| "SUBZONE_NAME", | |
| "SZ", | |
| "SZ_NAME", | |
| "SUBZONE NAME", | |
| ] | |
| for col in df.columns: | |
| if col.upper().strip() in possible_names or "SUBZONE" in col.upper(): | |
| return col | |
| return None | |
| def parse_poi_coordinates( | |
| df_theme: pd.DataFrame, | |
| ) -> Optional[gpd.GeoDataFrame]: | |
| """Parses coordinates from LatLng, Lat/Lng columns, or SVY21 metric columns into EPSG:3414.""" | |
| poi_gdf = None | |
| col_map = {c.upper().strip(): c for c in df_theme.columns} | |
| # Strategy 1: LatLng string column parsing | |
| if "LatLng" in df_theme.columns: | |
| extracted_floats = ( | |
| df_theme["LatLng"].dropna().astype(str).str.findall(r"(-?\d+\.\d+)") | |
| ) | |
| valid_pairs = extracted_floats[extracted_floats.str.len() >= 2] | |
| if not valid_pairs.empty: | |
| v1 = pd.to_numeric(valid_pairs.str[0], errors="coerce") | |
| v2 = pd.to_numeric(valid_pairs.str[1], errors="coerce") | |
| mask1 = (v1 >= 1.1) & (v1 <= 1.5) & (v2 >= 103.5) & (v2 <= 104.1) | |
| mask2 = (v2 >= 1.1) & (v2 <= 1.5) & (v1 >= 103.5) & (v1 <= 104.1) | |
| lats, lngs, valid_idx = None, None, None | |
| if mask1.any(): | |
| lats, lngs, valid_idx = v1[mask1], v2[mask1], v1[mask1].index | |
| elif mask2.any(): | |
| lats, lngs, valid_idx = v2[mask2], v1[mask2], v2[mask2].index | |
| if lats is not None and not lats.empty: | |
| df_filtered = df_theme.loc[valid_idx] | |
| poi_gdf = gpd.GeoDataFrame( | |
| df_filtered, | |
| geometry=gpd.points_from_xy(x=lngs, y=lats), | |
| crs="EPSG:4326", | |
| ).to_crs(epsg=3414) | |
| # Strategy 2: Lat / Long split columns | |
| if poi_gdf is None: | |
| lat_key = next( | |
| (col_map[k] for k in ["LATITUDE", "LAT"] if k in col_map), None | |
| ) | |
| lng_key = next( | |
| ( | |
| col_map[k] | |
| for k in ["LONGITUDE", "LNG", "LON", "LONGTITUDE"] | |
| if k in col_map | |
| ), | |
| None, | |
| ) | |
| if lat_key and lng_key: | |
| valid_rows = df_theme.dropna(subset=[lat_key, lng_key]).copy() | |
| lats = pd.to_numeric(valid_rows[lat_key], errors="coerce") | |
| lngs = pd.to_numeric(valid_rows[lng_key], errors="coerce") | |
| valid_mask = ( | |
| (lats >= 1.1) | |
| & (lats <= 1.5) | |
| & (lngs >= 103.5) | |
| & (lngs <= 104.1) | |
| ) | |
| if valid_mask.any(): | |
| df_filtered = valid_rows.loc[valid_mask] | |
| poi_gdf = gpd.GeoDataFrame( | |
| df_filtered, | |
| geometry=gpd.points_from_xy( | |
| lngs[valid_mask], lats[valid_mask] | |
| ), | |
| crs="EPSG:4326", | |
| ).to_crs(epsg=3414) | |
| # Strategy 3: Native SVY21 X / Y coordinates | |
| if poi_gdf is None: | |
| x_key = next( | |
| ( | |
| col_map[k] | |
| for k in ["X", "X_ADDR", "LANDXADDRESSPOINT"] | |
| if k in col_map | |
| ), | |
| None, | |
| ) | |
| y_key = next( | |
| ( | |
| col_map[k] | |
| for k in ["Y", "Y_ADDR", "LANDYADDRESSPOINT"] | |
| if k in col_map | |
| ), | |
| None, | |
| ) | |
| if x_key and y_key: | |
| valid_xy = df_theme.dropna(subset=[x_key, y_key]).copy() | |
| x_vals = pd.to_numeric(valid_xy[x_key], errors="coerce") | |
| y_vals = pd.to_numeric(valid_xy[y_key], errors="coerce") | |
| mask_svy21 = (x_vals > 10000) & (y_vals > 10000) | |
| if mask_svy21.any(): | |
| df_svy21 = valid_xy.loc[mask_svy21] | |
| poi_gdf = gpd.GeoDataFrame( | |
| df_svy21, | |
| geometry=gpd.points_from_xy( | |
| x_vals[mask_svy21], y_vals[mask_svy21] | |
| ), | |
| crs="EPSG:3414", | |
| ) | |
| return poi_gdf | |
| def load_data(input_dir: str) -> Dict[str, Any]: | |
| """Loads raw vector datasets, CSVs, and micro-themes from input directory.""" | |
| print(f"📥 Loading raw datasets from: {input_dir}", flush=True) | |
| datasets = {} | |
| # 1. Base Geometry Frame | |
| subzone_path = os.path.join(input_dir, "subzone_boundary_mp19.geojson") | |
| if not os.path.exists(subzone_path): | |
| raise FileNotFoundError(f"❌ Missing mandatory spatial boundary: {subzone_path}") | |
| datasets["subzones"] = gpd.read_file(subzone_path).to_crs(epsg=3414) | |
| datasets["subzones"]["SUBZONE_N"] = clean_subzone_series(datasets["subzones"]["SUBZONE_N"]) | |
| datasets["subzones"]["area_sqkm"] = datasets["subzones"].geometry.area / 1e6 | |
| # 2. Demographics Data | |
| dwell_path = os.path.join(input_dir, "pop_dwelling_census2020.csv") | |
| datasets["dwelling"] = pd.read_csv(dwell_path) if os.path.exists(dwell_path) else None | |
| age_path = os.path.join(input_dir, "pop_age_sex_census2020.csv") | |
| datasets["age"] = pd.read_csv(age_path) if os.path.exists(age_path) else None | |
| # 3. Spatial Land Use | |
| land_path = os.path.join(input_dir, "land_use_mp19.geojson") | |
| datasets["land_use"] = gpd.read_file(land_path).to_crs(epsg=3414) if os.path.exists(land_path) else None | |
| # 4. Public Transit Infrastructure & OD Telemetry | |
| bus_path = os.path.join(input_dir, "lta_bus_stops.csv") | |
| datasets["bus_stops"] = pd.read_csv(bus_path) if os.path.exists(bus_path) else None | |
| rail_path = os.path.join(input_dir, "rail_stations_mp19.geojson") | |
| datasets["rail_stations"] = gpd.read_file(rail_path).to_crs(epsg=3414) if os.path.exists(rail_path) else None | |
| od_bus_path = os.path.join(input_dir, "lta_od_bus.csv") | |
| datasets["od_bus"] = pd.read_csv(od_bus_path) if os.path.exists(od_bus_path) else None | |
| od_train_path = os.path.join(input_dir, "lta_od_train.csv") | |
| datasets["od_train"] = pd.read_csv(od_train_path) if os.path.exists(od_train_path) else None | |
| # 5. Education Infrastructure | |
| school_path = os.path.join(input_dir, "general_info_schools.csv") | |
| datasets["schools"] = pd.read_csv(school_path) if os.path.exists(school_path) else None | |
| # 6. OneMap Micro-Themes Data | |
| datasets["poi_themes"] = {} | |
| for theme_code in KEY_POI_THEMES.keys(): | |
| theme_file = os.path.join(input_dir, f"onemap_{theme_code}.csv") | |
| if os.path.exists(theme_file): | |
| datasets["poi_themes"][theme_code] = pd.read_csv(theme_file) | |
| return datasets | |
| def build_feature_matrix(input_dir: str) -> pd.DataFrame: | |
| """Constructs subzone feature matrix by consuming raw datasets.""" | |
| datasets = load_data(input_dir) | |
| # 1. Base Master Frame | |
| subzones = datasets["subzones"] | |
| master_df = subzones[["SUBZONE_N", "PLN_AREA_N", "REGION_N", "area_sqkm"]].copy() | |
| # 2. Demographics - Housing / Wealth | |
| dwell_df = datasets.get("dwelling") | |
| if dwell_df is not None: | |
| sub_col = get_subzone_col(dwell_df) or "Number" | |
| dwell_df["SUBZONE_N"] = clean_subzone_series(dwell_df[sub_col]) | |
| dwell_df = dwell_df[ | |
| ~dwell_df["SUBZONE_N"].str.contains( | |
| "- TOTAL|TOTAL", case=False, na=False | |
| ) | |
| ].copy() | |
| num_cols = [ | |
| c | |
| for c in dwell_df.columns | |
| if c | |
| not in [ | |
| "Number", | |
| "SUBZONE_N", | |
| "PLN_AREA_N", | |
| "Planning Area", | |
| "Total", | |
| ] | |
| ] | |
| for c in num_cols: | |
| dwell_df[c] = pd.to_numeric( | |
| dwell_df[c].astype(str).str.replace(",", ""), errors="coerce" | |
| ).fillna(0) | |
| condo_cols = [ | |
| c | |
| for c in num_cols | |
| if any( | |
| k in c.upper() for k in ["CONDO", "APARTMENT", "LANDED"] | |
| ) | |
| ] | |
| hdb_cols = [c for c in num_cols if "HDB" in c.upper()] | |
| dwell_df["private_pop"] = ( | |
| dwell_df[condo_cols].sum(axis=1) if condo_cols else 0 | |
| ) | |
| dwell_df["hdb_pop"] = dwell_df[hdb_cols].sum(axis=1) if hdb_cols else 0 | |
| total_dwell = dwell_df["private_pop"] + dwell_df["hdb_pop"] | |
| dwell_df["private_housing_ratio"] = np.where( | |
| total_dwell > 0, dwell_df["private_pop"] / total_dwell, 0.0 | |
| ) | |
| dwell_df["hdb_housing_ratio"] = np.where( | |
| total_dwell > 0, dwell_df["hdb_pop"] / total_dwell, 0.0 | |
| ) | |
| dwell_clean = dwell_df[ | |
| ["SUBZONE_N", "private_housing_ratio", "hdb_housing_ratio"] | |
| ].drop_duplicates("SUBZONE_N") | |
| master_df = master_df.merge( | |
| dwell_clean, on="SUBZONE_N", how="left" | |
| ).fillna({"private_housing_ratio": 0.0, "hdb_housing_ratio": 0.0}) | |
| # 3. Demographics - Life-stage / Age Ratios | |
| age_df = datasets.get("age") | |
| if age_df is not None: | |
| sub_col = get_subzone_col(age_df) or "Number" | |
| age_df["SUBZONE_N"] = clean_subzone_series(age_df[sub_col]) | |
| age_df = age_df[ | |
| ~age_df["SUBZONE_N"].str.contains( | |
| "- TOTAL|TOTAL", case=False, na=False | |
| ) | |
| ].copy() | |
| num_cols = [ | |
| c | |
| for c in age_df.columns | |
| if c | |
| not in [ | |
| "Number", | |
| "SUBZONE_N", | |
| "PLN_AREA_N", | |
| "Planning Area", | |
| "Sex", | |
| "SEX", | |
| "Total", | |
| ] | |
| ] | |
| for c in num_cols: | |
| age_df[c] = pd.to_numeric( | |
| age_df[c].astype(str).str.replace(",", ""), errors="coerce" | |
| ).fillna(0) | |
| age_subzone = age_df.groupby("SUBZONE_N")[num_cols].sum().reset_index() | |
| young_targets = [ | |
| "0_to_4", "5_to_9", "10_to_14", "0 - 4", "5 - 9", "10 - 14", "0_4", "5_9", "10_14" | |
| ] | |
| young_cols = [ | |
| c for c in num_cols if any(t in c.lower() for t in young_targets) | |
| ] | |
| elderly_targets = ["65", "70", "75", "80", "85", "90"] | |
| elderly_cols = [ | |
| c for c in num_cols if any(t in c.lower() for t in elderly_targets) | |
| ] | |
| total_pop = age_subzone[num_cols].sum(axis=1) | |
| age_subzone["young_family_ratio"] = np.where( | |
| total_pop > 0, age_subzone[young_cols].sum(axis=1) / total_pop, 0.0 | |
| ) | |
| age_subzone["elderly_ratio"] = np.where( | |
| total_pop > 0, age_subzone[elderly_cols].sum(axis=1) / total_pop, 0.0 | |
| ) | |
| age_clean = age_subzone[ | |
| ["SUBZONE_N", "young_family_ratio", "elderly_ratio"] | |
| ].drop_duplicates("SUBZONE_N") | |
| master_df = master_df.merge( | |
| age_clean, on="SUBZONE_N", how="left" | |
| ).fillna({"young_family_ratio": 0.0, "elderly_ratio": 0.0}) | |
| # 4. Commercial Land Area | |
| land_use = datasets.get("land_use") | |
| if land_use is not None: | |
| comm_land = land_use[ | |
| land_use["LU_DESC"].isin( | |
| ["COMMERCIAL", "COMMERCIAL & RESIDENTIAL", "WHITE"] | |
| ) | |
| ].copy() | |
| comm_land["comm_area_sqm"] = comm_land.geometry.area | |
| comm_joined = gpd.sjoin( | |
| comm_land, subzones[["SUBZONE_N", "geometry"]], predicate="intersects" | |
| ) | |
| comm_summary = ( | |
| comm_joined.groupby("SUBZONE_N")["comm_area_sqm"] | |
| .sum() | |
| .rename("comm_land_sqm") | |
| ) | |
| master_df = master_df.merge(comm_summary, on="SUBZONE_N", how="left").fillna( | |
| {"comm_land_sqm": 0.0} | |
| ) | |
| else: | |
| master_df["comm_land_sqm"] = 0.0 | |
| master_df["comm_land_ratio"] = master_df["comm_land_sqm"] / ( | |
| master_df["area_sqkm"] * 1e6 | |
| ) | |
| master_df["comm_land_density"] = ( | |
| master_df["comm_land_sqm"] / master_df["area_sqkm"] | |
| ) | |
| # 5. Transit Nodes & OD Telemetry | |
| bus_joined, rail_joined = None, None | |
| bus_stops = datasets.get("bus_stops") | |
| if bus_stops is not None: | |
| bus_gdf = gpd.GeoDataFrame( | |
| bus_stops, | |
| geometry=gpd.points_from_xy( | |
| bus_stops["Longitude"], bus_stops["Latitude"] | |
| ), | |
| crs="EPSG:4326", | |
| ).to_crs(epsg=3414) | |
| bus_joined = gpd.sjoin( | |
| bus_gdf, subzones[["SUBZONE_N", "geometry"]], predicate="within" | |
| ) | |
| bus_counts = ( | |
| bus_joined.groupby("SUBZONE_N").size().rename("bus_stop_count") | |
| ) | |
| master_df = master_df.merge(bus_counts, on="SUBZONE_N", how="left").fillna( | |
| {"bus_stop_count": 0} | |
| ) | |
| else: | |
| master_df["bus_stop_count"] = 0 | |
| rail_stations = datasets.get("rail_stations") | |
| if rail_stations is not None: | |
| rail_joined = gpd.sjoin( | |
| rail_stations, | |
| subzones[["SUBZONE_N", "geometry"]], | |
| predicate="intersects", | |
| ) | |
| rail_counts = ( | |
| rail_joined.groupby("SUBZONE_N").size().rename("mrt_station_count") | |
| ) | |
| master_df = master_df.merge( | |
| rail_counts, on="SUBZONE_N", how="left" | |
| ).fillna({"mrt_station_count": 0}) | |
| else: | |
| master_df["mrt_station_count"] = 0 | |
| master_df["bus_stop_density"] = ( | |
| master_df["bus_stop_count"] / master_df["area_sqkm"] | |
| ) | |
| master_df["mrt_station_density"] = ( | |
| master_df["mrt_station_count"] / master_df["area_sqkm"] | |
| ) | |
| for od_key, name in [ | |
| ("od_bus", "bus_trips"), | |
| ("od_train", "train_trips"), | |
| ]: | |
| od_df = datasets.get(od_key) | |
| target_joined = bus_joined if "bus" in name else rail_joined | |
| if ( | |
| od_df is not None | |
| and target_joined is not None | |
| and not target_joined.empty | |
| ): | |
| trip_cols = [ | |
| c for c in od_df.columns if any(k in c.upper() for k in ["TRIP", "TOTAL", "PASSENGER", "VOL"]) | |
| ] | |
| dest_cols = [ | |
| c for c in od_df.columns if any(k in c.upper() for k in ["DEST", "PT_CODE", "STATION", "STOP"]) | |
| ] | |
| if trip_cols and dest_cols: | |
| trip_col, dest_col = trip_cols[0], dest_cols[0] | |
| code_col = target_joined.columns[0] | |
| if "bus" in name and "BusStopCode" in target_joined.columns: | |
| code_col = "BusStopCode" | |
| elif "train" in name: | |
| rail_code_cols = [ | |
| c for c in target_joined.columns if any(k in c.upper() for k in ["STN", "CODE", "STATION"]) | |
| ] | |
| if rail_code_cols: | |
| code_col = rail_code_cols[0] | |
| od_df[dest_col] = ( | |
| od_df[dest_col].astype(str).str.strip().str.replace(r"\.0$", "", regex=True) | |
| ) | |
| target_sub = target_joined[[code_col, "SUBZONE_N"]].copy() | |
| target_sub[code_col] = ( | |
| target_sub[code_col].astype(str).str.strip().str.replace(r"\.0$", "", regex=True) | |
| ) | |
| od_merged = od_df.merge( | |
| target_sub, left_on=dest_col, right_on=code_col, how="inner" | |
| ) | |
| vol_summary = ( | |
| od_merged.groupby("SUBZONE_N")[trip_col] | |
| .sum() | |
| .rename(f"ambient_{name}") | |
| ) | |
| master_df = master_df.merge( | |
| vol_summary, on="SUBZONE_N", how="left" | |
| ).fillna({f"ambient_{name}": 0.0}) | |
| if "ambient_bus_trips" not in master_df.columns: | |
| master_df["ambient_bus_trips"] = 0.0 | |
| if "ambient_train_trips" not in master_df.columns: | |
| master_df["ambient_train_trips"] = 0.0 | |
| master_df["ambient_bus_trips_density"] = ( | |
| master_df["ambient_bus_trips"] / master_df["area_sqkm"] | |
| ) | |
| master_df["ambient_train_trips_density"] = ( | |
| master_df["ambient_train_trips"] / master_df["area_sqkm"] | |
| ) | |
| # 6. Education Infrastructure | |
| school_df = datasets.get("schools") | |
| if school_df is not None: | |
| sub_sch_col = get_subzone_col(school_df) | |
| if sub_sch_col: | |
| school_df["SUBZONE_N"] = clean_subzone_series(school_df[sub_sch_col]) | |
| sch_counts = ( | |
| school_df.groupby("SUBZONE_N").size().rename("school_count") | |
| ) | |
| master_df = master_df.merge( | |
| sch_counts, on="SUBZONE_N", how="left" | |
| ).fillna({"school_count": 0}) | |
| else: | |
| master_df["school_count"] = 0 | |
| else: | |
| master_df["school_count"] = 0 | |
| master_df["school_density"] = master_df["school_count"] / master_df["area_sqkm"] | |
| # 7. OneMap Spatial POI Micro-Themes | |
| subzones_3414 = subzones[["SUBZONE_N", "geometry"]].copy() | |
| subzones_3414.sindex | |
| poi_themes = datasets.get("poi_themes", {}) | |
| for theme_code, feature_col in KEY_POI_THEMES.items(): | |
| df_theme = poi_themes.get(theme_code) | |
| if df_theme is not None and not df_theme.empty: | |
| poi_gdf = parse_poi_coordinates(df_theme) | |
| if poi_gdf is not None and not poi_gdf.empty: | |
| try: | |
| joined = gpd.sjoin( | |
| poi_gdf, | |
| subzones_3414, | |
| predicate="intersects", | |
| how="inner", | |
| ) | |
| if not joined.empty: | |
| joined["SUBZONE_N"] = clean_subzone_series( | |
| joined["SUBZONE_N"] | |
| ) | |
| theme_counts = ( | |
| joined.groupby("SUBZONE_N") | |
| .size() | |
| .rename(feature_col) | |
| ) | |
| master_df = master_df.merge( | |
| theme_counts, on="SUBZONE_N", how="left" | |
| ).fillna({feature_col: 0}) | |
| else: | |
| master_df[feature_col] = 0 | |
| except Exception as e: | |
| print(f"⚠️ Spatial join failed for theme {theme_code}: {e}", flush=True) | |
| master_df[feature_col] = 0 | |
| else: | |
| master_df[feature_col] = 0 | |
| else: | |
| master_df[feature_col] = 0 | |
| poi_cols = [c for c in master_df.columns if c.startswith("poi_")] | |
| master_df["total_micro_poi_density"] = master_df[poi_cols].sum(axis=1) / master_df["area_sqkm"] | |
| for poi_col in poi_cols: | |
| master_df[f"{poi_col}_density"] = master_df[poi_col] / master_df["area_sqkm"] | |
| print(f"⚙️ Raw Feature Matrix assembled: {master_df.shape}", flush=True) | |
| return master_df | |
| def process_raw_features( | |
| master_df: pd.DataFrame, | |
| output_dir: str = DEFAULT_OUTPUT_DIR, | |
| ) -> pd.DataFrame: | |
| """Calculates outlier caps and exports unscaled raw features for model training/Gradia.""" | |
| print("\n" + "=" * 80) | |
| print("STAGE 2: RAW FEATURE CLEANING & OUTLIER CAPPING SETUP") | |
| print("=" * 80, flush=True) | |
| metadata_cols = ["SUBZONE_N", "PLN_AREA_N", "REGION_N"] | |
| feature_cols = [c for c in master_df.columns if c not in metadata_cols] | |
| X_raw = master_df[feature_cols].copy() | |
| X_raw = X_raw.replace([np.inf, -np.inf], np.nan).fillna(0.0) | |
| # 1. Outlier Clipping (Save caps dictionary for inference) | |
| print("--> Computing outlier caps (1st-99th percentile)...", flush=True) | |
| caps = {} | |
| for col in X_raw.columns: | |
| lower_cap = float(X_raw[col].quantile(0.01)) | |
| upper_cap = float(X_raw[col].quantile(0.99)) | |
| caps[col] = (lower_cap, upper_cap) | |
| # 2. Save feature caps artifact for inference.py live transformations | |
| os.makedirs(output_dir, exist_ok=True) | |
| joblib.dump(caps, os.path.join(output_dir, "feature_caps.joblib")) | |
| print(f"💾 Saved feature caps artifact (feature_caps.joblib) to {output_dir}", flush=True) | |
| # 3. Format unscaled raw dataset | |
| raw_df = pd.concat([master_df[metadata_cols], X_raw], axis=1) | |
| print(f"--> Exporting raw feature dataset with {len(feature_cols)} features.\n", flush=True) | |
| return raw_df | |
| def preprocess_workflow( | |
| input_dir: str = DEFAULT_INPUT_DIR, | |
| output_dir: str = DEFAULT_OUTPUT_DIR, | |
| ): | |
| """Orchestrates loading raw inputs, spatial processing, and saving raw features.""" | |
| print("🚀 Initiating Preprocessing Step...", flush=True) | |
| os.makedirs(output_dir, exist_ok=True) | |
| # 1. Load raw datasets to grab base boundary layer | |
| datasets = load_data(input_dir) | |
| subzones_gdf = datasets["subzones"] | |
| # 2. Export GeoJSON spatial boundaries to processed output directory | |
| geojson_output_path = os.path.join(output_dir, "subzone_boundary.geojson") | |
| subzones_gdf.to_crs(epsg=4326).to_file(geojson_output_path, driver="GeoJSON") | |
| print(f"🗺️ Spatial boundary GeoJSON saved to: {geojson_output_path}", flush=True) | |
| # 3. Assemble raw features | |
| raw_master_df = build_feature_matrix(input_dir=input_dir) | |
| raw_df = process_raw_features( | |
| master_df=raw_master_df, | |
| output_dir=output_dir | |
| ) | |
| # 4. Save unscaled raw features matrix | |
| raw_filepath = os.path.join(output_dir, "raw_features.csv") | |
| raw_df.to_csv(raw_filepath, index=False) | |
| print(f"💾 Unscaled raw features matrix saved to: {raw_filepath}", flush=True) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--input-data", type=str, default=DEFAULT_INPUT_DIR, help="Path to input raw data folder") | |
| parser.add_argument("--output-data", type=str, default=DEFAULT_OUTPUT_DIR, help="Path to output processed data folder") | |
| args, _ = parser.parse_known_args() | |
| preprocess_workflow( | |
| input_dir=args.input_data, | |
| output_dir=args.output_data, | |
| ) |