XinyiC11 commited on
Commit
ac58e5c
·
verified ·
1 Parent(s): f682bdc

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +76 -76
src/streamlit_app.py CHANGED
@@ -3,17 +3,11 @@ import pandas as pd
3
  import altair as alt
4
  import json
5
  import urllib.request
6
- import os
7
-
8
- # 🌟 关键:解除 Altair 5000 行的限制,允许柱状图和折线图使用全量数据渲染
9
- alt.data_transformers.disable_max_rows()
10
 
11
  st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="🚨", layout="wide")
12
-
13
  st.title("Crimes in Chicago - 2026")
14
  st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
15
  st.markdown("---")
16
-
17
  st.markdown(
18
  """
19
  ## What Is This About?
@@ -22,77 +16,78 @@ st.markdown(
22
  This interactive article walks you through 2026 Chicago crime data drawn directly from
23
  the [Chicago Data Portal](https://data.cityofchicago.org/) to help you explore the
24
  geography, timing, and social context of crime in one of America's largest cities.
 
 
 
 
25
  """
26
  )
27
 
28
  # ---------------------------------------------------------------------------
29
  # Data loading
30
  # ---------------------------------------------------------------------------
31
-
32
- @st.cache_data(show_spinner="Loading Chicago crime data from CSV...")
33
  def load_crime_data():
34
- """自动寻找目录下的 Crimes 数据集,彻底告别文件名拼写报错"""
35
-
36
- # 1. 自动扫描当前文件夹
37
- current_files = os.listdir('.')
38
- csv_files = [f for f in current_files if 'Crimes' in f and f.endswith('.csv')]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- # 2. 如果什么都没找到,把当前文件夹里到底有什么打印出来,方便排错
41
- if not csv_files:
42
- st.error(f"找不到 CSV 文件!当前文件夹里只有这些文件: {current_files}")
43
  return pd.DataFrame()
44
-
45
- # 3. 抓取找到的第一个文件
46
- target_file = csv_files[0]
47
- st.success(f"自动寻路成功!正在读取: {target_file}")
48
 
49
- try:
50
- df_raw = pd.read_csv(target_file, low_memory=False)
51
- except Exception as e:
52
- st.error(f"文件找到了,但读取失败: {e}")
53
- return pd.DataFrame()
54
-
55
- df = df_raw.copy()
56
 
57
- # 适配 CSV 列名
58
- df["date"] = pd.to_datetime(df["Date"], format='%m/%d/%Y %I:%M:%S %p', errors="coerce")
59
- for col, orig_col in [("latitude", "Latitude"), ("longitude", "Longitude")]:
60
- if orig_col in df.columns:
61
- df[col] = pd.to_numeric(df[orig_col], errors="coerce")
62
-
63
  df = df.dropna(subset=["date"])
64
-
65
- # 🌟 修复 Pandas 'd' 为 'D' 弃用警告
66
- df["Date_Only"] = df["date"].dt.floor("D")
67
  df["Hour"] = df["date"].dt.hour
68
  df["weekday"] = df["date"].dt.day_name().str[:3]
69
-
70
- df["Primary Type"] = df["Primary Type"].str.upper() if "Primary Type" in df.columns else "UNKNOWN"
71
-
72
- if "District" in df.columns:
73
  df["District_Str"] = (
74
- pd.to_numeric(df["District"], errors="coerce")
75
  .fillna(-1).astype(int).astype(str)
76
  )
77
  df["District"] = df["District_Str"]
78
  else:
79
  df["District_Str"] = df["District"] = "-1"
80
-
81
- if "Community Area" in df.columns:
82
- df["community_area"] = df["Community Area"]
83
- else:
84
  df["community_area"] = None
85
-
86
  def get_period(h):
87
  if 6 < h <= 12: return "Morning (6am-12pm)"
88
  elif 12 < h <= 18: return "Afternoon (12pm-6pm)"
89
  elif 18 < h <= 24: return "Evening (6pm-12am)"
90
  else: return "Late Night (12am-6am)"
91
-
92
  df["Period"] = df["Hour"].apply(get_period)
93
  return df
94
 
95
-
96
  @st.cache_data(show_spinner="Loading socioeconomic data...")
97
  def load_socio():
98
  try:
@@ -105,7 +100,6 @@ def load_socio():
105
  st.warning(f"Could not load socioeconomic data: {e}")
106
  return pd.DataFrame(columns=["ca", "community_area_name", "poverty_rate"])
107
 
108
-
109
  @st.cache_data(show_spinner="Loading boundaries...")
110
  def load_geojson(url):
111
  try:
@@ -115,7 +109,6 @@ def load_geojson(url):
115
  st.warning(f"Could not load GeoJSON: {e}")
116
  return {"features": []}
117
 
118
-
119
  district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson")
120
  community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson")
121
 
@@ -125,7 +118,7 @@ districts = alt.Data(values=district_geojson["features"])
125
  communities = alt.Data(values=community_geojson["features"])
126
 
127
  if df.empty:
128
- st.error("Crime data could not be loaded. Please check the logs.")
129
  st.stop()
130
 
131
  df_geo = df.dropna(subset=["latitude", "longitude"]).copy()
@@ -151,8 +144,6 @@ st.markdown(
151
  brush = alt.selection_interval(name="brush")
152
  click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
153
  click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
154
-
155
- # 地图图层采样 (保护浏览器),但右侧柱状图/折线图坚持用全量数据 df
156
  MAP_SAMPLE = 5000
157
  df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
158
 
@@ -192,10 +183,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)",
196
  )
197
 
198
- # 🌟 Bar chart - 坚持使用全量数据集 df
199
  type_chart = (
200
  alt.Chart(df)
201
  .mark_bar()
@@ -211,7 +202,7 @@ type_chart = (
211
  .transform_filter(click_dist)
212
  )
213
 
214
- # 🌟 Line chart - 坚持使用全量数据集 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,8 +254,6 @@ 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
-
267
- # 修复旧版警告,使用 width="stretch" (如果报错可改回 use_container_width=True)
268
  st.altair_chart(dashboard, use_container_width=True)
269
 
270
  # ---------------------------------------------------------------------------
@@ -278,6 +267,9 @@ 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
  """
282
  )
283
 
@@ -308,7 +300,7 @@ heatmap = (
308
  )
309
  .properties(
310
  width=700, height=380,
311
- title=f"Crime Heatmap - {selected_hm} (full dataset)",
312
  )
313
  )
314
  st.altair_chart(heatmap, use_container_width=True)
@@ -322,8 +314,17 @@ st.markdown(
322
  """
323
  Socioeconomic inequality is one of the most studied predictors of crime at the
324
  neighborhood level. The choropleth map on the left shades each of Chicago's 77
325
- community areas by their poverty rate.
326
- The scatter plot on the right makes the poverty-crime relationship explicit.
 
 
 
 
 
 
 
 
 
327
  """
328
  )
329
 
@@ -349,16 +350,20 @@ with col3:
349
  .project(type="mercator")
350
  .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
351
  )
352
-
353
- # 🌟 核心修复:预先聚合经纬度,解决 longitude/latitude 不能应用 bin 的严重报错问题!
 
354
  df_geo_binned = df_geo.copy()
355
- df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(2)
356
  df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(2)
357
- density_agg = df_geo_binned.groupby(['lon_bin', 'lat_bin']).size().reset_index(name='incident_count')
 
 
 
358
 
 
359
  crime_density = (
360
  alt.Chart(density_agg)
361
- .mark_square(opacity=0.7)
362
  .encode(
363
  longitude="lon_bin:Q",
364
  latitude="lat_bin:Q",
@@ -368,19 +373,12 @@ with col3:
368
  title="Incident Count",
369
  legend=alt.Legend(title="Incidents"),
370
  ),
371
- size=alt.Size(
372
- "incident_count:Q",
373
- scale=alt.Scale(range=[10, 150]),
374
- legend=None
375
- ),
376
  tooltip=[
377
- alt.Tooltip("lon_bin:Q", title="Longitude (Grid)"),
378
- alt.Tooltip("lat_bin:Q", title="Latitude (Grid)"),
379
- alt.Tooltip("incident_count:Q", title="Incidents")
380
  ]
381
  )
382
  )
383
-
384
  st.altair_chart(
385
  (poverty_map + crime_density).resolve_scale(color="independent"),
386
  use_container_width=True,
@@ -398,11 +396,13 @@ with col4:
398
  df_crime_count["ca"] = (
399
  df_crime_count["community_area"].astype(float).astype(int).astype(str)
400
  )
 
401
  df_scatter = pd.merge(
402
  df_socio[["ca", "community_area_name", "poverty_rate"]],
403
  df_crime_count[["ca", "crime_count"]],
404
  on="ca", how="inner",
405
  )
 
406
  if len(df_scatter) > 5:
407
  sc = (
408
  alt.Chart(df_scatter)
@@ -425,7 +425,7 @@ with col4:
425
  st.altair_chart(
426
  (sc + reg).properties(
427
  width=360, height=440,
428
- title="Higher Poverty -> More Crimes?",
429
  ),
430
  use_container_width=True,
431
  )
 
3
  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
  st.title("Crimes in Chicago - 2026")
9
  st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
10
  st.markdown("---")
 
11
  st.markdown(
12
  """
13
  ## What Is This About?
 
16
  This interactive article walks you through 2026 Chicago crime data drawn directly from
17
  the [Chicago Data Portal](https://data.cityofchicago.org/) to help you explore the
18
  geography, timing, and social context of crime in one of America's largest cities.
19
+ The dataset records every reported crime incident in 2026, including the exact location,
20
+ date and time, crime type, and the police district that handled it. Each row is one
21
+ reported incident. We also include community-level socioeconomic data to examine the
22
+ relationship between poverty and crime rates across Chicago's neighborhoods.
23
  """
24
  )
25
 
26
  # ---------------------------------------------------------------------------
27
  # Data loading
28
  # ---------------------------------------------------------------------------
29
+ @st.cache_data(show_spinner="Loading Chicago crime data (full dataset)...")
 
30
  def load_crime_data():
31
+ """Fetch ALL 2026 records in batches. Bar/line/heatmap use the full df;
32
+ only geo rendering on maps uses sampling."""
33
+ all_chunks = []
34
+ limit = 50000
35
+ offset = 0
36
+ while True:
37
+ url = (
38
+ "https://data.cityofchicago.org/resource/ijzp-q8t2.json"
39
+ "?$where=year=2026"
40
+ f"&$limit={limit}"
41
+ f"&$offset={offset}"
42
+ "&$order=date%20DESC"
43
+ )
44
+ try:
45
+ chunk = pd.read_json(url)
46
+ except Exception as e:
47
+ st.error(f"Failed to load crime data at offset {offset}: {e}")
48
+ break
49
+ if chunk.empty:
50
+ break
51
+ all_chunks.append(chunk)
52
+ if len(chunk) < limit:
53
+ break
54
+ offset += limit
55
 
56
+ if not all_chunks:
 
 
57
  return pd.DataFrame()
 
 
 
 
58
 
59
+ df = pd.concat(all_chunks, ignore_index=True)
60
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
61
+ for col in ["latitude", "longitude"]:
62
+ df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
 
 
 
63
 
 
 
 
 
 
 
64
  df = df.dropna(subset=["date"])
65
+ df["Date_Only"] = df["date"].dt.floor("d")
 
 
66
  df["Hour"] = df["date"].dt.hour
67
  df["weekday"] = df["date"].dt.day_name().str[:3]
68
+ df["Primary Type"] = df["primary_type"].str.upper() if "primary_type" in df.columns else "UNKNOWN"
69
+
70
+ if "district" in df.columns:
 
71
  df["District_Str"] = (
72
+ pd.to_numeric(df["district"], errors="coerce")
73
  .fillna(-1).astype(int).astype(str)
74
  )
75
  df["District"] = df["District_Str"]
76
  else:
77
  df["District_Str"] = df["District"] = "-1"
78
+
79
+ if "community_area" not in df.columns:
 
 
80
  df["community_area"] = None
81
+
82
  def get_period(h):
83
  if 6 < h <= 12: return "Morning (6am-12pm)"
84
  elif 12 < h <= 18: return "Afternoon (12pm-6pm)"
85
  elif 18 < h <= 24: return "Evening (6pm-12am)"
86
  else: return "Late Night (12am-6am)"
87
+
88
  df["Period"] = df["Hour"].apply(get_period)
89
  return df
90
 
 
91
  @st.cache_data(show_spinner="Loading socioeconomic data...")
92
  def load_socio():
93
  try:
 
100
  st.warning(f"Could not load socioeconomic data: {e}")
101
  return pd.DataFrame(columns=["ca", "community_area_name", "poverty_rate"])
102
 
 
103
  @st.cache_data(show_spinner="Loading boundaries...")
104
  def load_geojson(url):
105
  try:
 
109
  st.warning(f"Could not load GeoJSON: {e}")
110
  return {"features": []}
111
 
 
112
  district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson")
113
  community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson")
114
 
 
118
  communities = alt.Data(values=community_geojson["features"])
119
 
120
  if df.empty:
121
+ st.error("Crime data could not be loaded.")
122
  st.stop()
123
 
124
  df_geo = df.dropna(subset=["latitude", "longitude"]).copy()
 
144
  brush = alt.selection_interval(name="brush")
145
  click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
146
  click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
 
 
147
  MAP_SAMPLE = 5000
148
  df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
149
 
 
183
 
184
  map_layer = (background + geo_points).project(type="mercator").properties(
185
  width=420, height=450,
186
+ title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)",
187
  )
188
 
189
+ # Bar chart - full df
190
  type_chart = (
191
  alt.Chart(df)
192
  .mark_bar()
 
202
  .transform_filter(click_dist)
203
  )
204
 
205
+ # Line chart - full df
206
  period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
207
  "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
208
  period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
 
254
  ).resolve_scale(color="shared")
255
 
256
  dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
 
 
257
  st.altair_chart(dashboard, use_container_width=True)
258
 
259
  # ---------------------------------------------------------------------------
 
267
  the heatmap by crime category, or leave it on *All* to see the overall pattern.
268
  Each cell shows the total number of incidents at that day-of-week x hour-of-day
269
  combination across the full dataset; darker red means more incidents.
270
+ Across nearly every category, Friday and Saturday evenings (6 pm to midnight)
271
+ stand out as the most active windows, while the early-morning hours (2 to 5 am)
272
+ are consistently quietest.
273
  """
274
  )
275
 
 
300
  )
301
  .properties(
302
  width=700, height=380,
303
+ title=f"Crime Heatmap - {selected_hm} (full dataset, darker = more incidents)",
304
  )
305
  )
306
  st.altair_chart(heatmap, use_container_width=True)
 
314
  """
315
  Socioeconomic inequality is one of the most studied predictors of crime at the
316
  neighborhood level. The choropleth map on the left shades each of Chicago's 77
317
+ community areas by their poverty rate - darker orange means higher poverty -
318
+ with a binned crime density heatmap overlaid. The heatmap uses the full dataset
319
+ with no sampling: each cell's color reflects how many incidents fall in that
320
+ geographic bin, giving a clear picture of crime hotspots.
321
+
322
+ The scatter plot on the right makes the poverty-crime relationship explicit:
323
+ each dot is one community area, and the dashed line is a statistical trend.
324
+ There is a moderate positive correlation, though it is far from deterministic -
325
+ policy, policing patterns, and reporting rates all play a role.
326
+
327
+ **Socioeconomic data source:** [Census Data - Chicago Data Portal](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2)
328
  """
329
  )
330
 
 
350
  .project(type="mercator")
351
  .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
352
  )
353
+
354
+ # --- FIX: Pre-aggregate data for geographic binning in Pandas ---
355
+ # 舍入到 2 位小数创建了与 maxbins=50 相似的地理网格
356
  df_geo_binned = df_geo.copy()
 
357
  df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(2)
358
+ df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(2)
359
+
360
+ # 统计每个坐标网格的案件数量
361
+ density_agg = df_geo_binned.groupby(['lat_bin', 'lon_bin']).size().reset_index(name='incident_count')
362
 
363
+ # Binned geo-heatmap: 使用 mark_square 模拟网格热力图
364
  crime_density = (
365
  alt.Chart(density_agg)
366
+ .mark_square(opacity=0.65, size=80)
367
  .encode(
368
  longitude="lon_bin:Q",
369
  latitude="lat_bin:Q",
 
373
  title="Incident Count",
374
  legend=alt.Legend(title="Incidents"),
375
  ),
 
 
 
 
 
376
  tooltip=[
377
+ alt.Tooltip("incident_count:Q", title="Total Incidents")
 
 
378
  ]
379
  )
380
  )
381
+
382
  st.altair_chart(
383
  (poverty_map + crime_density).resolve_scale(color="independent"),
384
  use_container_width=True,
 
396
  df_crime_count["ca"] = (
397
  df_crime_count["community_area"].astype(float).astype(int).astype(str)
398
  )
399
+
400
  df_scatter = pd.merge(
401
  df_socio[["ca", "community_area_name", "poverty_rate"]],
402
  df_crime_count[["ca", "crime_count"]],
403
  on="ca", how="inner",
404
  )
405
+
406
  if len(df_scatter) > 5:
407
  sc = (
408
  alt.Chart(df_scatter)
 
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
  )