8282_DEFAULT_RENT = str (_RENT_CAND ) if _RENT_CAND .is_file () else str (_FIX_RENT )
8383_PLANNING_GEO = _ROOT / "data" / "reference" / "planning_areas.geojson"
8484_TINY_GEO = _ROOT / "tests" / "fixtures" / "planning_areas_tiny.geojson"
85+ _REQUIRED_RESALE_COLS = {"month" , "resale_price" }
8586
8687
8788def _is_truthy (v : str | None ) -> bool :
@@ -125,12 +126,29 @@ def _default_geo() -> str:
125126
126127
127128def _bootstrap_resale_if_missing (default_path : str ) -> str :
129+ def _has_required_cols (p : Path ) -> bool :
130+ try :
131+ cols = pd .read_csv (p , nrows = 0 ).columns
132+ except (OSError , ValueError , pd .errors .ParserError ):
133+ return False
134+ norm = {str (c ).strip ().lower ().replace (" " , "_" ) for c in cols }
135+ return _REQUIRED_RESALE_COLS .issubset (norm )
136+
128137 p = Path (default_path )
129- if p .exists ():
138+ if p .exists () and _has_required_cols ( p ) :
130139 return default_path
140+ if p .exists () and not _has_required_cols (p ):
141+ st .sidebar .warning (
142+ f"CSV at `{ p } ` does not look like HDB resale data "
143+ f"(needs columns: { sorted (_REQUIRED_RESALE_COLS )} )."
144+ )
131145 if _ALT_BIG_CSV .exists ():
132- st .sidebar .info (f"Using bundled resale CSV: `{ _ALT_BIG_CSV } `" )
133- return str (_ALT_BIG_CSV )
146+ if _has_required_cols (_ALT_BIG_CSV ):
147+ st .sidebar .info (f"Using bundled resale CSV: `{ _ALT_BIG_CSV } `" )
148+ return str (_ALT_BIG_CSV )
149+ st .sidebar .warning (
150+ f"Bundled CSV `{ _ALT_BIG_CSV } ` is not HDB-format; skipping."
151+ )
134152
135153 auto_fetch = _default_bool_env (
136154 "SINGAPORE_EDA_AUTO_DOWNLOAD_ON_MISSING" ,
@@ -154,8 +172,12 @@ def _bootstrap_resale_if_missing(default_path: str) -> str:
154172 skip_if_fresh_hours = 24.0 ,
155173 )
156174 st .sidebar .success (f"Fetched { n :,} resale rows from data.gov.sg." )
157- if p .exists ():
175+ if p .exists () and _has_required_cols ( p ) :
158176 return str (p )
177+ if p .exists ():
178+ st .sidebar .warning (
179+ f"Downloaded file `{ p } ` is missing required HDB columns; falling back."
180+ )
159181 except (OSError , ValueError , RuntimeError ) as ex :
160182 st .sidebar .warning (f"Auto-download failed; using fixture fallback. ({ ex } )" )
161183
@@ -264,7 +286,8 @@ def _ols_coef_table(model: Any, top_n: int = 12) -> pd.DataFrame:
264286 out = pd .DataFrame (rows )
265287 # Keep continuous drivers first, then strongest remaining terms by magnitude.
266288 priority = ["floor_area_sqm" , "remaining_lease_years" , "C(storey_band)" ]
267- pri = out [out ["term" ].str .contains ("|" .join (priority ), regex = True )].copy ()
289+ pri_mask = out ["term" ].map (lambda t : any (k in str (t ) for k in priority ))
290+ pri = out [pri_mask ].copy ()
268291 rest = out [~ out .index .isin (pri .index )].copy ()
269292 rest = rest .sort_values (by = "approx_price_change_pct" , key = lambda s : s .abs (), ascending = False )
270293 out2 = pd .concat ([pri , rest .head (max (0 , top_n - len (pri )))], ignore_index = True )
@@ -516,6 +539,15 @@ def main() -> None:
516539 except FileNotFoundError as e :
517540 st .error (str (e ))
518541 st .stop ()
542+ except KeyError as e :
543+ st .error (
544+ "Selected CSV is missing required columns for HDB processing "
545+ f"(missing: { e } ). Please choose an HDB resale CSV."
546+ )
547+ st .stop ()
548+ except ValueError as e :
549+ st .error (f"Could not parse selected CSV: { e } " )
550+ st .stop ()
519551
520552 (
521553 tab_pm ,
0 commit comments