aunghlaing commited on
Commit
cfd7b0b
Β·
verified Β·
1 Parent(s): fa382c6

Createapp/py

Browse files
Files changed (1) hide show
  1. app.py +210 -0
app.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import geopandas as gpd
5
+
6
+ st.set_page_config(layout="wide", page_title="Singapore Housing Data Dashboard", page_icon=":house:")
7
+
8
+ @st.cache_data
9
+ def load_data():
10
+ # Use relative paths for deployment
11
+ df = pd.read_csv("Adjusted_Resale_Prices_2025_with_coords.csv")
12
+ gdf = gpd.read_file("planning_area_boundaries.geojson")
13
+ return df, gdf
14
+
15
+ try:
16
+ df, gdf = load_data()
17
+ except FileNotFoundError as e:
18
+ st.error(f"Data file not found: {e}")
19
+ st.stop()
20
+
21
+ st.title("🏠 Singapore HDB Resale Price Dashboard")
22
+ st.markdown("---")
23
+
24
+ # Sidebar Filters
25
+ st.sidebar.header("πŸ” Filters")
26
+
27
+ # Check if required columns exist
28
+ required_columns = ["planning_area", "flat_type", "storey_range", "lease_commence_year", "resale_price"]
29
+ missing_columns = [col for col in required_columns if col not in df.columns]
30
+
31
+ if missing_columns:
32
+ st.error(f"Missing required columns: {missing_columns}")
33
+ st.write("Available columns:", list(df.columns))
34
+ st.stop()
35
+
36
+ all_planning_areas = sorted(df["planning_area"].unique())
37
+ selected_planning_area = st.sidebar.selectbox(
38
+ "Planning Area",
39
+ ["All"] + all_planning_areas
40
+ )
41
+
42
+ all_flat_types = sorted(df["flat_type"].unique())
43
+ selected_flat_types = st.sidebar.multiselect(
44
+ "Flat Type",
45
+ all_flat_types,
46
+ default=all_flat_types
47
+ )
48
+
49
+ all_storey_ranges = sorted(df["storey_range"].unique())
50
+ selected_storey_ranges = st.sidebar.multiselect(
51
+ "Storey Range",
52
+ all_storey_ranges,
53
+ default=all_storey_ranges
54
+ )
55
+
56
+ min_year = int(df["lease_commence_year"].min())
57
+ max_year = int(df["lease_commence_year"].max())
58
+ selected_year_range = st.sidebar.slider(
59
+ "Lease Commencement Year",
60
+ min_year, max_year, (min_year, max_year)
61
+ )
62
+
63
+ # Apply filters
64
+ filtered_df = df[
65
+ (df["flat_type"].isin(selected_flat_types)) &
66
+ (df["storey_range"].isin(selected_storey_ranges)) &
67
+ (df["lease_commence_year"] >= selected_year_range[0]) &
68
+ (df["lease_commence_year"] <= selected_year_range[1])
69
+ ]
70
+
71
+ if selected_planning_area != "All":
72
+ filtered_df = filtered_df[filtered_df["planning_area"] == selected_planning_area]
73
+
74
+ # Display filter summary
75
+ st.sidebar.markdown("---")
76
+ st.sidebar.write(f"**Records shown:** {len(filtered_df):,}")
77
+ st.sidebar.write(f"**Total records:** {len(df):,}")
78
+
79
+ # Main content
80
+ col1, col2 = st.columns([2, 1])
81
+
82
+ with col2:
83
+ if not filtered_df.empty:
84
+ avg_price = filtered_df["resale_price"].mean()
85
+ median_price = filtered_df["resale_price"].median()
86
+ max_price = filtered_df["resale_price"].max()
87
+ min_price = filtered_df["resale_price"].min()
88
+
89
+ st.metric("Average Price", f"${avg_price:,.0f}")
90
+ st.metric("Median Price", f"${median_price:,.0f}")
91
+ st.metric("Price Range", f"${min_price:,.0f} - ${max_price:,.0f}")
92
+
93
+ with col1:
94
+ st.header("πŸ“Š Key Statistics")
95
+
96
+ # Choropleth Map
97
+ st.header("πŸ—ΊοΈ Average Resale Price by Planning Area")
98
+ if not filtered_df.empty:
99
+ avg_price_by_planning_area = filtered_df.groupby("planning_area")["resale_price"].mean().reset_index()
100
+
101
+ # Try to merge with GeoDataFrame
102
+ try:
103
+ # Check if the GeoDataFrame has the expected column
104
+ if "PLN_AREA_N" in gdf.columns:
105
+ gdf_merged = gdf.merge(avg_price_by_planning_area, left_on="PLN_AREA_N", right_on="planning_area", how="left")
106
+ else:
107
+ # Try other common column names
108
+ possible_columns = [col for col in gdf.columns if "area" in col.lower() or "name" in col.lower()]
109
+ if possible_columns:
110
+ gdf_merged = gdf.merge(avg_price_by_planning_area, left_on=possible_columns[0], right_on="planning_area", how="left")
111
+ else:
112
+ st.error("Could not find matching column in GeoJSON for planning areas")
113
+ st.write("GeoJSON columns:", list(gdf.columns))
114
+ gdf_merged = None
115
+
116
+ if gdf_merged is not None:
117
+ fig_map = px.choropleth_mapbox(
118
+ gdf_merged,
119
+ geojson=gdf_merged.geometry,
120
+ locations=gdf_merged.index,
121
+ color="resale_price",
122
+ color_continuous_scale="Viridis",
123
+ mapbox_style="carto-positron",
124
+ zoom=9.5,
125
+ center={"lat": 1.3521, "lon": 103.8198},
126
+ opacity=0.7,
127
+ labels={
128
+ "resale_price": "Avg Resale Price (SGD)",
129
+ },
130
+ hover_name=gdf_merged.columns[0] if "PLN_AREA_N" not in gdf_merged.columns else "PLN_AREA_N",
131
+ hover_data={
132
+ "resale_price": ":$,.0f",
133
+ }
134
+ )
135
+ fig_map.update_layout(margin={"r":0,"t":0,"l":0,"b":0}, height=500)
136
+ st.plotly_chart(fig_map, use_container_width=True)
137
+ else:
138
+ st.warning("Could not create choropleth map due to data structure mismatch.")
139
+ except Exception as e:
140
+ st.error(f"Error creating map: {e}")
141
+ st.write("Showing data table instead:")
142
+ st.dataframe(avg_price_by_planning_area)
143
+ else:
144
+ st.warning("No data to display for the selected filters on the map.")
145
+
146
+ # Line Chart
147
+ st.header("πŸ“ˆ Resale Price Trends Over Lease Commencement Year")
148
+ if not filtered_df.empty:
149
+ avg_price_by_year = filtered_df.groupby("lease_commence_year")["resale_price"].mean().reset_index()
150
+ fig_line = px.line(
151
+ avg_price_by_year,
152
+ x="lease_commence_year",
153
+ y="resale_price",
154
+ title="Average Resale Price by Lease Commencement Year",
155
+ labels={
156
+ "lease_commence_year": "Lease Commencement Year",
157
+ "resale_price": "Average Resale Price (SGD)"
158
+ },
159
+ markers=True
160
+ )
161
+ fig_line.update_traces(mode="lines+markers", line=dict(width=3))
162
+ fig_line.update_layout(
163
+ hovermode="x unified",
164
+ height=400,
165
+ xaxis_title="Lease Commencement Year",
166
+ yaxis_title="Average Resale Price (SGD)"
167
+ )
168
+ st.plotly_chart(fig_line, use_container_width=True)
169
+ else:
170
+ st.warning("No data to display for the selected filters on the line chart.")
171
+
172
+ # Additional Charts
173
+ if not filtered_df.empty:
174
+ col1, col2 = st.columns(2)
175
+
176
+ with col1:
177
+ st.subheader("πŸ“Š Price Distribution by Flat Type")
178
+ fig_box = px.box(
179
+ filtered_df,
180
+ x="flat_type",
181
+ y="resale_price",
182
+ title="Price Distribution by Flat Type"
183
+ )
184
+ fig_box.update_layout(height=400)
185
+ st.plotly_chart(fig_box, use_container_width=True)
186
+
187
+ with col2:
188
+ st.subheader("🏒 Average Price by Storey Range")
189
+ avg_by_storey = filtered_df.groupby("storey_range")["resale_price"].mean().reset_index()
190
+ fig_bar = px.bar(
191
+ avg_by_storey,
192
+ x="storey_range",
193
+ y="resale_price",
194
+ title="Average Price by Storey Range"
195
+ )
196
+ fig_bar.update_layout(height=400)
197
+ st.plotly_chart(fig_bar, use_container_width=True)
198
+
199
+ # Toggle for Data Table
200
+ st.header("πŸ“‹ Filtered Data Table")
201
+ show_data_table = st.checkbox("Show filtered data table")
202
+ if show_data_table:
203
+ if not filtered_df.empty:
204
+ st.dataframe(filtered_df, use_container_width=True)
205
+ else:
206
+ st.info("No data to display in the table for the selected filters.")
207
+
208
+ # Footer
209
+ st.markdown("---")
210
+ st.markdown("*Data source: Singapore HDB Resale Prices*")