-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstorey.py
More file actions
78 lines (67 loc) · 2.55 KB
/
Copy pathstorey.py
File metadata and controls
78 lines (67 loc) · 2.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
"""Storey / floor-band analysis (HDB `storey_range` is a band, not exact floor)."""
from __future__ import annotations
import pandas as pd
from singapore_eda.constants import SQM_TO_SQFT
def add_storey_band(df: pd.DataFrame) -> pd.DataFrame:
"""Ordered categories: low / mid / high from storey_mid when available."""
out = df.copy()
if "storey_mid" not in out.columns:
return out
sm = out["storey_mid"]
out["storey_band"] = pd.cut(
sm,
bins=[-1, 3, 9, 100],
labels=["low_1_3", "mid_4_9", "high_10plus"],
).astype(str)
out.loc[sm.isna(), "storey_band"] = pd.NA
return out
def median_price_by_storey_stratum(
df: pd.DataFrame,
*,
area_bins: int = 5,
) -> pd.DataFrame:
"""Stratify by floor-area quantiles within sample; median price by storey_band."""
need = {"resale_price", "floor_area_sqm", "storey_band"}
if not need.issubset(set(df.columns)):
return pd.DataFrame()
d = df.dropna(subset=list(need)).copy()
if d.empty:
return pd.DataFrame()
try:
q = min(area_bins, len(d))
d["area_stratum"] = pd.qcut(d["floor_area_sqm"], q=q, duplicates="drop")
except ValueError:
d["area_stratum"] = "all"
g = d.groupby(["area_stratum", "storey_band"], observed=True).agg(
median_price=("resale_price", "median")
)
out = g.reset_index()
out["area_stratum (m²)"] = out["area_stratum"].map(_format_area_stratum)
out["area_stratum (sqft)"] = out["area_stratum"].map(_format_area_stratum_sqft)
out = out.drop(columns=["area_stratum"], errors="ignore")
if "median_price" in out.columns:
out = out.rename(columns={"median_price": "median_resale"})
return out
def _format_area_stratum(x) -> str:
"""Turn qcut / Interval / mixed types into a readable m² range for tables."""
if x is None or (isinstance(x, float) and pd.isna(x)):
return ""
if hasattr(x, "left") and hasattr(x, "right"):
a, b = float(x.left), float(x.right)
return f"{a:.0f}–{b:.0f} m²"
s = str(x)
if s == "all":
return "all (single bin)"
return s
def _format_area_stratum_sqft(x) -> str:
"""Same bins as m², expressed in square feet (common in Singapore marketing)."""
if x is None or (isinstance(x, float) and pd.isna(x)):
return ""
if hasattr(x, "left") and hasattr(x, "right"):
a = float(x.left) * SQM_TO_SQFT
b = float(x.right) * SQM_TO_SQFT
return f"{a:.0f}–{b:.0f} sqft"
s = str(x)
if s == "all":
return "all (single bin)"
return s