XinyiC11 commited on
Commit
969812c
·
verified ·
1 Parent(s): 9a5fcaf

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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
- # Debug信息(第一次部署时非常重要)
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
- The bottom line chart breaks daily incident counts into four time-of-day periods
154
- (plus a grey total line). The bar chart and line chart use the **full dataset** with
155
- no sampling; only the map points are sampled to keep the browser responsive.
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, or leave it on *All* to see the overall pattern.
283
- Each cell shows the total number of incidents at that day-of-week x hour-of-day
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
- weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
295
- hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
296
  hm_agg = (
297
- hm_source
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
- color=alt.Color("crime_count:Q", scale=alt.Scale(scheme="reds"), title="Number of Crimes"),
 
310
  tooltip=[
311
- alt.Tooltip("weekday:N", title="Day"),
312
- alt.Tooltip("Hour:O", title="Hour"),
313
- alt.Tooltip("crime_count:Q", title="Total Crimes"),
314
  ],
315
  )
 
 
316
  .properties(
317
  width=700, height=380,
318
- title=f"Crime Heatmap - {selected_hm} (full dataset, darker = more incidents)",
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
- # --- Pre-aggregate data for geographic binning in Pandas ---
370
- # 舍入到 2 位小数创建了与 maxbins=50 相似的地理网格
371
  df_geo_binned = df_geo.copy()
372
- df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(2)
373
- df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(2)
 
374
 
375
- # 统计每个坐标网格的案件数量
376
  density_agg = df_geo_binned.groupby(['lat_bin', 'lon_bin']).size().reset_index(name='incident_count')
377
 
378
- # Binned geo-heatmap: 使用 mark_square 模拟网格热力图
379
  crime_density = (
380
  alt.Chart(density_agg)
381
- .mark_square(opacity=0.65, size=80)
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",