Update src/streamlit_app.py
Browse files- src/streamlit_app.py +38 -41
src/streamlit_app.py
CHANGED
|
@@ -27,25 +27,16 @@ st.markdown(
|
|
| 27 |
# Data loading (Now using local CSV for extreme speedup)
|
| 28 |
# ---------------------------------------------------------------------------
|
| 29 |
@st.cache_data(show_spinner="Loading local Chicago crime data...")
|
| 30 |
-
@st.cache_data(show_spinner="Loading local Chicago crime data...")
|
| 31 |
def load_crime_data():
|
| 32 |
"""Robust loading for Hugging Face Spaces (handles path issues)."""
|
| 33 |
import os
|
| 34 |
|
| 35 |
try:
|
| 36 |
-
#
|
| 37 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 38 |
-
|
| 39 |
-
# ✅ 构造绝对路径
|
| 40 |
file_path = os.path.join(BASE_DIR, "Crimes_-_2026_20260417.csv")
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
st.write("📂 Current working dir:", os.getcwd())
|
| 44 |
-
st.write("📂 BASE_DIR:", BASE_DIR)
|
| 45 |
-
st.write("📄 Files in BASE_DIR:", os.listdir(BASE_DIR))
|
| 46 |
-
st.write("📄 Trying to read:", file_path)
|
| 47 |
-
|
| 48 |
-
# ✅ 读取 CSV
|
| 49 |
df = pd.read_csv(file_path)
|
| 50 |
|
| 51 |
# 标准化列名
|
|
@@ -58,9 +49,7 @@ def load_crime_data():
|
|
| 58 |
st.error(f"❌ Failed to read CSV: {e}")
|
| 59 |
return pd.DataFrame()
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
# 后处理(你原来的逻辑)
|
| 63 |
-
# -------------------------
|
| 64 |
df["date"] = pd.to_datetime(df["date"], errors="coerce")
|
| 65 |
|
| 66 |
for col in ["latitude", "longitude"]:
|
|
@@ -150,9 +139,9 @@ st.markdown(
|
|
| 150 |
**Drag a box on the map** to select a geographic area, or **click a district boundary**
|
| 151 |
to highlight it — both actions filter the bar chart on the right and the timeline below.
|
| 152 |
You can also **click a crime category** in the bar chart to drill into its temporal trend.
|
| 153 |
-
|
| 154 |
-
(
|
| 155 |
-
|
| 156 |
"""
|
| 157 |
)
|
| 158 |
|
|
@@ -272,50 +261,58 @@ dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="indepen
|
|
| 272 |
st.altair_chart(dashboard, use_container_width=True)
|
| 273 |
|
| 274 |
# ---------------------------------------------------------------------------
|
| 275 |
-
# SECTION 2 — When do crimes happen? heatmap + dropdown
|
| 276 |
# ---------------------------------------------------------------------------
|
| 277 |
st.markdown("---")
|
| 278 |
st.header("When Do Crimes Happen in Chicago?")
|
| 279 |
st.markdown(
|
| 280 |
"""
|
| 281 |
Different crimes follow different schedules. Use the **dropdown below** to filter
|
| 282 |
-
the heatmap by crime category
|
| 283 |
-
|
| 284 |
-
combination across the full dataset; darker red means more incidents.
|
| 285 |
-
Across nearly every category, Friday and Saturday evenings (6 pm to midnight)
|
| 286 |
-
stand out as the most active windows, while the early-morning hours (2 to 5 am)
|
| 287 |
-
are consistently quietest.
|
| 288 |
"""
|
| 289 |
)
|
| 290 |
|
|
|
|
| 291 |
top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
|
| 292 |
-
selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
|
| 293 |
|
| 294 |
-
|
| 295 |
-
hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
|
| 296 |
hm_agg = (
|
| 297 |
-
|
| 298 |
-
.groupby(["weekday", "Hour"])
|
| 299 |
.size()
|
| 300 |
.reset_index(name="crime_count")
|
| 301 |
)
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
heatmap = (
|
| 304 |
alt.Chart(hm_agg)
|
| 305 |
.mark_rect()
|
| 306 |
.encode(
|
| 307 |
x=alt.X("weekday:N", sort=weekday_order, title="Day of Week"),
|
| 308 |
y=alt.Y("Hour:O", title="Hour of Day (0-23)", sort="ascending"),
|
| 309 |
-
|
|
|
|
| 310 |
tooltip=[
|
| 311 |
-
alt.Tooltip("weekday:N",
|
| 312 |
-
alt.Tooltip("Hour:O",
|
| 313 |
-
alt.Tooltip("crime_count:Q", title="Total Crimes"),
|
| 314 |
],
|
| 315 |
)
|
|
|
|
|
|
|
| 316 |
.properties(
|
| 317 |
width=700, height=380,
|
| 318 |
-
title=
|
| 319 |
)
|
| 320 |
)
|
| 321 |
st.altair_chart(heatmap, use_container_width=True)
|
|
@@ -366,19 +363,19 @@ with col3:
|
|
| 366 |
.properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
|
| 367 |
)
|
| 368 |
|
| 369 |
-
# ---
|
| 370 |
-
# 舍入到 2 位小数创建了与 maxbins=50 相似的地理网格
|
| 371 |
df_geo_binned = df_geo.copy()
|
| 372 |
-
|
| 373 |
-
df_geo_binned['
|
|
|
|
| 374 |
|
| 375 |
-
# 统计每个
|
| 376 |
density_agg = df_geo_binned.groupby(['lat_bin', 'lon_bin']).size().reset_index(name='incident_count')
|
| 377 |
|
| 378 |
-
# Binned geo-heatmap: 使用
|
| 379 |
crime_density = (
|
| 380 |
alt.Chart(density_agg)
|
| 381 |
-
.
|
| 382 |
.encode(
|
| 383 |
longitude="lon_bin:Q",
|
| 384 |
latitude="lat_bin:Q",
|
|
|
|
| 27 |
# Data loading (Now using local CSV for extreme speedup)
|
| 28 |
# ---------------------------------------------------------------------------
|
| 29 |
@st.cache_data(show_spinner="Loading local Chicago crime data...")
|
|
|
|
| 30 |
def load_crime_data():
|
| 31 |
"""Robust loading for Hugging Face Spaces (handles path issues)."""
|
| 32 |
import os
|
| 33 |
|
| 34 |
try:
|
| 35 |
+
# 获取当前脚本所在目录
|
| 36 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
|
| 37 |
file_path = os.path.join(BASE_DIR, "Crimes_-_2026_20260417.csv")
|
| 38 |
|
| 39 |
+
# 读取 CSV
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
df = pd.read_csv(file_path)
|
| 41 |
|
| 42 |
# 标准化列名
|
|
|
|
| 49 |
st.error(f"❌ Failed to read CSV: {e}")
|
| 50 |
return pd.DataFrame()
|
| 51 |
|
| 52 |
+
# 后处理
|
|
|
|
|
|
|
| 53 |
df["date"] = pd.to_datetime(df["date"], errors="coerce")
|
| 54 |
|
| 55 |
for col in ["latitude", "longitude"]:
|
|
|
|
| 139 |
**Drag a box on the map** to select a geographic area, or **click a district boundary**
|
| 140 |
to highlight it — both actions filter the bar chart on the right and the timeline below.
|
| 141 |
You can also **click a crime category** in the bar chart to drill into its temporal trend.
|
| 142 |
+
|
| 143 |
+
*(Note: If the map points look like a strict grid, it is because the Chicago Police Department
|
| 144 |
+
anonymizes crime locations to the nearest block level, aligning perfectly with Chicago's grid street system!)*
|
| 145 |
"""
|
| 146 |
)
|
| 147 |
|
|
|
|
| 261 |
st.altair_chart(dashboard, use_container_width=True)
|
| 262 |
|
| 263 |
# ---------------------------------------------------------------------------
|
| 264 |
+
# SECTION 2 — When do crimes happen? heatmap + dropdown (NOW LAG-FREE)
|
| 265 |
# ---------------------------------------------------------------------------
|
| 266 |
st.markdown("---")
|
| 267 |
st.header("When Do Crimes Happen in Chicago?")
|
| 268 |
st.markdown(
|
| 269 |
"""
|
| 270 |
Different crimes follow different schedules. Use the **dropdown below** to filter
|
| 271 |
+
the heatmap by crime category.
|
| 272 |
+
*(This is now fully interactive in your browser, filtering happens instantly without lag!)*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
"""
|
| 274 |
)
|
| 275 |
|
| 276 |
+
# 提取前10大罪案类型
|
| 277 |
top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
|
|
|
|
| 278 |
|
| 279 |
+
# 在 Python 预先计算所有 (Primary Type, weekday, Hour) 的聚合数量,减少传到前端的数据量
|
|
|
|
| 280 |
hm_agg = (
|
| 281 |
+
df.dropna(subset=["Primary Type"])
|
| 282 |
+
.groupby(["Primary Type", "weekday", "Hour"])
|
| 283 |
.size()
|
| 284 |
.reset_index(name="crime_count")
|
| 285 |
)
|
| 286 |
|
| 287 |
+
# 🔥 核心提速秘籍:创建一个 Altair 原生的下拉绑定,把过滤操作全推给前端浏览器做,不重启 Streamlit!
|
| 288 |
+
dropdown = alt.binding_select(
|
| 289 |
+
options=[None] + top_types_hm,
|
| 290 |
+
labels=["All"] + top_types_hm,
|
| 291 |
+
name="Filter by Crime Type: "
|
| 292 |
+
)
|
| 293 |
+
type_select = alt.selection_point(fields=["Primary Type"], bind=dropdown)
|
| 294 |
+
|
| 295 |
+
weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
| 296 |
+
|
| 297 |
heatmap = (
|
| 298 |
alt.Chart(hm_agg)
|
| 299 |
.mark_rect()
|
| 300 |
.encode(
|
| 301 |
x=alt.X("weekday:N", sort=weekday_order, title="Day of Week"),
|
| 302 |
y=alt.Y("Hour:O", title="Hour of Day (0-23)", sort="ascending"),
|
| 303 |
+
# 使用 sum(crime_count) 确保选 All 的时候数字正确累加
|
| 304 |
+
color=alt.Color("sum(crime_count):Q", scale=alt.Scale(scheme="reds"), title="Number of Crimes"),
|
| 305 |
tooltip=[
|
| 306 |
+
alt.Tooltip("weekday:N", title="Day"),
|
| 307 |
+
alt.Tooltip("Hour:O", title="Hour"),
|
| 308 |
+
alt.Tooltip("sum(crime_count):Q", title="Total Crimes"),
|
| 309 |
],
|
| 310 |
)
|
| 311 |
+
.add_params(type_select) # 绑定前端选择器
|
| 312 |
+
.transform_filter(type_select) # 让图表根据选择器过滤数据
|
| 313 |
.properties(
|
| 314 |
width=700, height=380,
|
| 315 |
+
title="Crime Heatmap (Instantly filterable)",
|
| 316 |
)
|
| 317 |
)
|
| 318 |
st.altair_chart(heatmap, use_container_width=True)
|
|
|
|
| 363 |
.properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
|
| 364 |
)
|
| 365 |
|
| 366 |
+
# --- FIX: 改成了更高精度的 round(3) 结合 mark_circle 来实现细腻的热力图外观 ---
|
|
|
|
| 367 |
df_geo_binned = df_geo.copy()
|
| 368 |
+
# round(3) 大约对应100米的网格,比原来的 1.1公里 (round 2) 精细很多
|
| 369 |
+
df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(3)
|
| 370 |
+
df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(3)
|
| 371 |
|
| 372 |
+
# 统计每个细微网格的案件数量
|
| 373 |
density_agg = df_geo_binned.groupby(['lat_bin', 'lon_bin']).size().reset_index(name='incident_count')
|
| 374 |
|
| 375 |
+
# Binned geo-heatmap: 使用半透明的小圆点(mark_circle)模拟完美的热力云图
|
| 376 |
crime_density = (
|
| 377 |
alt.Chart(density_agg)
|
| 378 |
+
.mark_circle(opacity=0.6, size=15) # 调小了size,换成了圆形
|
| 379 |
.encode(
|
| 380 |
longitude="lon_bin:Q",
|
| 381 |
latitude="lat_bin:Q",
|