XinyiC11 commited on
Commit
15ddace
·
verified ·
1 Parent(s): 0796022

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +59 -68
src/streamlit_app.py CHANGED
@@ -4,7 +4,10 @@ import altair as alt
4
  import json
5
  import urllib.request
6
 
7
- st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="?", layout="wide")
 
 
 
8
 
9
  st.title("Crimes in Chicago - 2026")
10
  st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
@@ -29,59 +32,45 @@ st.markdown(
29
  # Data loading
30
  # ---------------------------------------------------------------------------
31
 
32
- @st.cache_data(show_spinner="Loading Chicago crime data (full dataset)...")
33
  def load_crime_data():
34
- """Fetch ALL 2026 records in batches. Bar/line/heatmap use the full df;
35
- only geo rendering on maps uses sampling."""
36
- all_chunks = []
37
- limit = 50000
38
- offset = 0
39
- while True:
40
- url = (
41
- "https://data.cityofchicago.org/resource/ijzp-q8t2.json"
42
- "?$where=year=2026"
43
- f"&$limit={limit}"
44
- f"&$offset={offset}"
45
- "&$order=date%20DESC"
46
- )
47
- try:
48
- chunk = pd.read_json(url)
49
- except Exception as e:
50
- st.error(f"Failed to load crime data at offset {offset}: {e}")
51
- break
52
- if chunk.empty:
53
- break
54
- all_chunks.append(chunk)
55
- if len(chunk) < limit:
56
- break
57
- offset += limit
58
-
59
- if not all_chunks:
60
  return pd.DataFrame()
61
 
62
- df = pd.concat(all_chunks, ignore_index=True)
63
-
64
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
65
- for col in ["latitude", "longitude"]:
66
- df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
 
 
 
67
  df = df.dropna(subset=["date"])
68
 
69
- df["Date_Only"] = df["date"].dt.floor("d")
 
70
  df["Hour"] = df["date"].dt.hour
71
  df["weekday"] = df["date"].dt.day_name().str[:3]
72
 
73
- df["Primary Type"] = df["primary_type"].str.upper() if "primary_type" in df.columns else "UNKNOWN"
74
 
75
- if "district" in df.columns:
76
  df["District_Str"] = (
77
- pd.to_numeric(df["district"], errors="coerce")
78
  .fillna(-1).astype(int).astype(str)
79
  )
80
  df["District"] = df["District_Str"]
81
  else:
82
  df["District_Str"] = df["District"] = "-1"
83
 
84
- if "community_area" not in df.columns:
 
 
85
  df["community_area"] = None
86
 
87
  def get_period(h):
@@ -153,6 +142,7 @@ brush = alt.selection_interval(name="brush")
153
  click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
154
  click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
155
 
 
156
  MAP_SAMPLE = 5000
157
  df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
158
 
@@ -192,10 +182,10 @@ geo_points = (
192
 
193
  map_layer = (background + geo_points).project(type="mercator").properties(
194
  width=420, height=450,
195
- title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)",
196
  )
197
 
198
- # Bar chart - full df
199
  type_chart = (
200
  alt.Chart(df)
201
  .mark_bar()
@@ -211,7 +201,7 @@ type_chart = (
211
  .transform_filter(click_dist)
212
  )
213
 
214
- # Line chart - full df
215
  period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
216
  "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
217
  period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
@@ -263,12 +253,12 @@ line_chart = (total_line + period_lines).properties(
263
  ).resolve_scale(color="shared")
264
 
265
  dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
 
 
266
  st.altair_chart(dashboard, use_container_width=True)
267
 
268
  # ---------------------------------------------------------------------------
269
  # SECTION 2 — When do crimes happen? heatmap + dropdown
270
- # Pre-aggregate to 7x24 = 168 rows in Python before rendering,
271
- # so switching crime types is instant - no re-streaming of raw data to browser.
272
  # ---------------------------------------------------------------------------
273
  st.markdown("---")
274
  st.header("When Do Crimes Happen in Chicago?")
@@ -278,16 +268,12 @@ st.markdown(
278
  the heatmap by crime category, or leave it on *All* to see the overall pattern.
279
  Each cell shows the total number of incidents at that day-of-week x hour-of-day
280
  combination across the full dataset; darker red means more incidents.
281
- Across nearly every category, Friday and Saturday evenings (6 pm to midnight)
282
- stand out as the most active windows, while the early-morning hours (2 to 5 am)
283
- are consistently quietest.
284
  """
285
  )
286
 
287
  top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
288
  selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
289
 
290
- # Key fix: groupby in Python first -> only 168 rows reach Altair -> instant render
291
  weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
292
  hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
293
  hm_agg = (
@@ -312,15 +298,13 @@ heatmap = (
312
  )
313
  .properties(
314
  width=700, height=380,
315
- title=f"Crime Heatmap - {selected_hm} (full dataset, darker = more incidents)",
316
  )
317
  )
318
  st.altair_chart(heatmap, use_container_width=True)
319
 
320
  # ---------------------------------------------------------------------------
321
  # SECTION 3 — Poverty vs. Crime
322
- # Left: choropleth + binned geo-heatmap (no sampling, full density visible)
323
- # Right: scatter with full crime counts per community area
324
  # ---------------------------------------------------------------------------
325
  st.markdown("---")
326
  st.header("Does Poverty Predict Crime?")
@@ -328,15 +312,8 @@ st.markdown(
328
  """
329
  Socioeconomic inequality is one of the most studied predictors of crime at the
330
  neighborhood level. The choropleth map on the left shades each of Chicago's 77
331
- community areas by their poverty rate - darker orange means higher poverty -
332
- with a binned crime density heatmap overlaid. The heatmap uses the full dataset
333
- with no sampling: each cell's color reflects how many incidents fall in that
334
- geographic bin, giving a clear picture of crime hotspots.
335
- The scatter plot on the right makes the poverty-crime relationship explicit:
336
- each dot is one community area, and the dashed line is a statistical trend.
337
- There is a moderate positive correlation, though it is far from deterministic -
338
- policy, policing patterns, and reporting rates all play a role.
339
- **Socioeconomic data source:** [Census Data - Chicago Data Portal](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2)
340
  """
341
  )
342
 
@@ -363,20 +340,34 @@ with col3:
363
  .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
364
  )
365
 
366
- # Binned geo-heatmap: full dataset, no sampling needed
367
- # maxbins=50 -> ~2500 cells max, renders fast and shows full density
 
 
 
 
368
  crime_density = (
369
- alt.Chart(df_geo)
370
- .mark_rect(opacity=0.55)
371
  .encode(
372
- longitude=alt.X("longitude:Q", bin=alt.Bin(maxbins=50)),
373
- latitude=alt.Y("latitude:Q", bin=alt.Bin(maxbins=50)),
374
  color=alt.Color(
375
- "count():Q",
376
  scale=alt.Scale(scheme="blues"),
377
  title="Incident Count",
378
  legend=alt.Legend(title="Incidents"),
379
  ),
 
 
 
 
 
 
 
 
 
 
380
  )
381
  )
382
 
@@ -389,7 +380,6 @@ with col3:
389
 
390
  with col4:
391
  if not df_socio.empty and df["community_area"].notna().any():
392
- # Full df for crime counts - no sampling
393
  df_crime_count = (
394
  df.dropna(subset=["community_area"])
395
  .groupby("community_area").size()
@@ -425,7 +415,7 @@ with col4:
425
  st.altair_chart(
426
  (sc + reg).properties(
427
  width=360, height=440,
428
- title="Higher Poverty -> More Crimes? (each dot = one community area)",
429
  ),
430
  use_container_width=True,
431
  )
@@ -447,6 +437,7 @@ st.markdown(
447
  | Socioeconomic Indicators by Community | City of Chicago Data Portal | [kn9c-c2s2](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2) |
448
  | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
449
  | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
 
450
  All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
451
  """
452
  )
 
4
  import json
5
  import urllib.request
6
 
7
+ # 🌟 关键:解除 Altair 5000 行的限制,允许柱状图和折线图使用全量数据渲染
8
+ alt.data_transformers.disable_max_rows()
9
+
10
+ st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="🚨", layout="wide")
11
 
12
  st.title("Crimes in Chicago - 2026")
13
  st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
 
32
  # Data loading
33
  # ---------------------------------------------------------------------------
34
 
35
+ @st.cache_data(show_spinner="Loading Chicago crime data from CSV...")
36
  def load_crime_data():
37
+ """直接读取上传到 Hugging Face CSV 文件,不再使用 API 循环请求。"""
38
+ try:
39
+ # 读取本地上传的 CSV 文件
40
+ df_raw = pd.read_csv("Crimes_-_2026_20260417.csv", low_memory=False)
41
+ except Exception as e:
42
+ st.error(f"Failed to load CSV file. 请确保文件名准确并已上传到 Hugging Face: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  return pd.DataFrame()
44
 
45
+ df = df_raw.copy()
46
+
47
+ # 适配 CSV 列名
48
+ df["date"] = pd.to_datetime(df["Date"], format='%m/%d/%Y %I:%M:%S %p', errors="coerce")
49
+ for col, orig_col in [("latitude", "Latitude"), ("longitude", "Longitude")]:
50
+ if orig_col in df.columns:
51
+ df[col] = pd.to_numeric(df[orig_col], errors="coerce")
52
+
53
  df = df.dropna(subset=["date"])
54
 
55
+ # 🌟 修复 Pandas 'd' 为 'D' 弃用警告
56
+ df["Date_Only"] = df["date"].dt.floor("D")
57
  df["Hour"] = df["date"].dt.hour
58
  df["weekday"] = df["date"].dt.day_name().str[:3]
59
 
60
+ df["Primary Type"] = df["Primary Type"].str.upper() if "Primary Type" in df.columns else "UNKNOWN"
61
 
62
+ if "District" in df.columns:
63
  df["District_Str"] = (
64
+ pd.to_numeric(df["District"], errors="coerce")
65
  .fillna(-1).astype(int).astype(str)
66
  )
67
  df["District"] = df["District_Str"]
68
  else:
69
  df["District_Str"] = df["District"] = "-1"
70
 
71
+ if "Community Area" in df.columns:
72
+ df["community_area"] = df["Community Area"]
73
+ else:
74
  df["community_area"] = None
75
 
76
  def get_period(h):
 
142
  click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
143
  click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
144
 
145
+ # 地图图层采样 (保护浏览器),但右侧柱状图/折线图坚持用全量数据 df
146
  MAP_SAMPLE = 5000
147
  df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
148
 
 
182
 
183
  map_layer = (background + geo_points).project(type="mercator").properties(
184
  width=420, height=450,
185
+ title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points)",
186
  )
187
 
188
+ # 🌟 Bar chart - 坚持使用全量数据集 df
189
  type_chart = (
190
  alt.Chart(df)
191
  .mark_bar()
 
201
  .transform_filter(click_dist)
202
  )
203
 
204
+ # 🌟 Line chart - 坚持使用全量数据集 df
205
  period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
206
  "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
207
  period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
 
253
  ).resolve_scale(color="shared")
254
 
255
  dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
256
+
257
+ # 修复���版警告,使用 width="stretch" (如果报错可改回 use_container_width=True)
258
  st.altair_chart(dashboard, use_container_width=True)
259
 
260
  # ---------------------------------------------------------------------------
261
  # SECTION 2 — When do crimes happen? heatmap + dropdown
 
 
262
  # ---------------------------------------------------------------------------
263
  st.markdown("---")
264
  st.header("When Do Crimes Happen in Chicago?")
 
268
  the heatmap by crime category, or leave it on *All* to see the overall pattern.
269
  Each cell shows the total number of incidents at that day-of-week x hour-of-day
270
  combination across the full dataset; darker red means more incidents.
 
 
 
271
  """
272
  )
273
 
274
  top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
275
  selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
276
 
 
277
  weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
278
  hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
279
  hm_agg = (
 
298
  )
299
  .properties(
300
  width=700, height=380,
301
+ title=f"Crime Heatmap - {selected_hm} (full dataset)",
302
  )
303
  )
304
  st.altair_chart(heatmap, use_container_width=True)
305
 
306
  # ---------------------------------------------------------------------------
307
  # SECTION 3 — Poverty vs. Crime
 
 
308
  # ---------------------------------------------------------------------------
309
  st.markdown("---")
310
  st.header("Does Poverty Predict Crime?")
 
312
  """
313
  Socioeconomic inequality is one of the most studied predictors of crime at the
314
  neighborhood level. The choropleth map on the left shades each of Chicago's 77
315
+ community areas by their poverty rate.
316
+ The scatter plot on the right makes the poverty-crime relationship explicit.
 
 
 
 
 
 
 
317
  """
318
  )
319
 
 
340
  .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
341
  )
342
 
343
+ # 🌟 核心修复:预先聚合经纬度,解决 longitude/latitude 不能应用 bin 的严重报错问题!
344
+ df_geo_binned = df_geo.copy()
345
+ df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(2)
346
+ df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(2)
347
+ density_agg = df_geo_binned.groupby(['lon_bin', 'lat_bin']).size().reset_index(name='incident_count')
348
+
349
  crime_density = (
350
+ alt.Chart(density_agg)
351
+ .mark_square(opacity=0.7)
352
  .encode(
353
+ longitude="lon_bin:Q",
354
+ latitude="lat_bin:Q",
355
  color=alt.Color(
356
+ "incident_count:Q",
357
  scale=alt.Scale(scheme="blues"),
358
  title="Incident Count",
359
  legend=alt.Legend(title="Incidents"),
360
  ),
361
+ size=alt.Size(
362
+ "incident_count:Q",
363
+ scale=alt.Scale(range=[10, 150]),
364
+ legend=None
365
+ ),
366
+ tooltip=[
367
+ alt.Tooltip("lon_bin:Q", title="Longitude (Grid)"),
368
+ alt.Tooltip("lat_bin:Q", title="Latitude (Grid)"),
369
+ alt.Tooltip("incident_count:Q", title="Incidents")
370
+ ]
371
  )
372
  )
373
 
 
380
 
381
  with col4:
382
  if not df_socio.empty and df["community_area"].notna().any():
 
383
  df_crime_count = (
384
  df.dropna(subset=["community_area"])
385
  .groupby("community_area").size()
 
415
  st.altair_chart(
416
  (sc + reg).properties(
417
  width=360, height=440,
418
+ title="Higher Poverty -> More Crimes?",
419
  ),
420
  use_container_width=True,
421
  )
 
437
  | Socioeconomic Indicators by Community | City of Chicago Data Portal | [kn9c-c2s2](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2) |
438
  | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
439
  | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
440
+
441
  All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
442
  """
443
  )