Skip to content

Commit fe6edad

Browse files
committed
fix(streamlit): validate resale csv schema and handle invalid files
1 parent b1fb6bc commit fe6edad

1 file changed

Lines changed: 37 additions & 5 deletions

File tree

streamlit_app.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
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

8788
def _is_truthy(v: str | None) -> bool:
@@ -125,12 +126,29 @@ def _default_geo() -> str:
125126

126127

127128
def _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

Comments
 (0)