XinyiC11 commited on
Commit
56cc940
·
verified ·
1 Parent(s): e70f227

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +115 -365
src/streamlit_app.py CHANGED
@@ -3,465 +3,215 @@ import pandas as pd
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?
14
- Every day, hundreds of crime incidents are reported across Chicago's 77 community areas.
15
- But where do they happen? At what time? And does poverty play a role?
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 (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
- # 标准化列名
43
- df.columns = [c.lower().replace(" ", "_") for c in df.columns]
44
 
45
- except FileNotFoundError:
46
- st.error("❌ CSV file not found. Check filename and path.")
47
- return pd.DataFrame()
48
- except Exception as e:
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"]:
56
- df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
57
 
58
  df = df.dropna(subset=["date"])
59
 
60
- df["Date_Only"] = df["date"].dt.floor("d")
61
- df["Hour"] = df["date"].dt.hour
62
  df["weekday"] = df["date"].dt.day_name().str[:3]
63
 
64
- df["Primary Type"] = (
65
- df["primary_type"].str.upper()
66
- if "primary_type" in df.columns else "UNKNOWN"
67
- )
68
-
69
- if "district" in df.columns:
70
- df["District_Str"] = (
71
- pd.to_numeric(df["district"], errors="coerce")
72
- .fillna(-1).astype(int).astype(str)
73
- )
74
- df["District"] = df["District_Str"]
75
- else:
76
- df["District_Str"] = df["District"] = "-1"
77
 
78
- if "community_area" not in df.columns:
79
- df["community_area"] = None
80
 
81
  def get_period(h):
82
  if 6 < h <= 12:
83
- return "Morning (6am-12pm)"
84
  elif 12 < h <= 18:
85
- return "Afternoon (12pm-6pm)"
86
  elif 18 < h <= 24:
87
- return "Evening (6pm-12am)"
88
  else:
89
- return "Late Night (12am-6am)"
90
 
91
- df["Period"] = df["Hour"].apply(get_period)
92
 
93
  return df
94
 
 
95
  @st.cache_data(show_spinner="Loading socioeconomic data...")
96
  def load_socio():
97
- try:
98
- df = pd.read_json("https://data.cityofchicago.org/resource/kn9c-c2s2.json")
99
- df = df.dropna(subset=["ca"])
100
- df["ca"] = df["ca"].astype(float).astype(int).astype(str)
101
- df["poverty_rate"] = pd.to_numeric(df["percent_households_below_poverty"], errors="coerce")
102
- return df
103
- except Exception as e:
104
- st.warning(f"Could not load socioeconomic data: {e}")
105
- return pd.DataFrame(columns=["ca", "community_area_name", "poverty_rate"])
106
 
107
  @st.cache_data(show_spinner="Loading boundaries...")
108
  def load_geojson(url):
109
- try:
110
- with urllib.request.urlopen(url) as r:
111
- return json.loads(r.read())
112
- except Exception as e:
113
- st.warning(f"Could not load GeoJSON: {e}")
114
- return {"features": []}
115
-
116
- district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson")
117
  community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson")
118
 
119
- df = load_crime_data()
120
  df_socio = load_socio()
121
- districts = alt.Data(values=district_geojson["features"])
 
122
  communities = alt.Data(values=community_geojson["features"])
123
 
124
  if df.empty:
125
- st.error("Crime data could not be loaded.")
126
  st.stop()
127
 
128
- df_geo = df.dropna(subset=["latitude", "longitude"]).copy()
129
- st.info(f"Loaded **{len(df):,}** crime records for 2026 ({len(df_geo):,} with coordinates).")
 
130
 
131
  # ---------------------------------------------------------------------------
132
- # SECTION 1 — Linked dashboard
133
  # ---------------------------------------------------------------------------
134
- st.markdown("---")
135
  st.header("Interactive Crime Dashboard")
136
- st.markdown(
137
- """
138
- This dashboard lets you explore Chicago crime data across three linked views.
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
 
148
- brush = alt.selection_interval(name="brush")
149
- click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
150
- click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
151
- MAP_SAMPLE = 5000
152
- df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
153
 
154
- background = (
155
- alt.Chart(districts)
156
- .mark_geoshape(stroke="black", strokeWidth=0.6)
157
- .transform_calculate(District_Str="datum.properties.dist_num")
158
- .encode(
159
- color=alt.condition(click_dist, alt.value("white"), alt.value("grey")),
160
- opacity=alt.condition(click_dist, alt.value(0.5), alt.value(0.8)),
161
- tooltip=[alt.Tooltip("properties.dist_num:N", title="District")],
162
- )
163
- .add_params(click_dist)
164
- )
165
 
166
- geo_points = (
167
- alt.Chart(df_map_sample)
168
  .mark_circle(size=5)
169
  .encode(
170
  longitude="longitude:Q",
171
  latitude="latitude:Q",
172
- color=alt.condition(
173
- click_dist,
174
- alt.Color("District:N", scale=alt.Scale(scheme="tableau10"),
175
- legend=alt.Legend(title="District", orient="right")),
176
- alt.value("#e0dbd6"),
177
- ),
178
- opacity=alt.condition(click_dist, alt.value(0.6), alt.value(0.05)),
179
- tooltip=[
180
- alt.Tooltip("Primary Type:N", title="Crime Type"),
181
- alt.Tooltip("District:N", title="District"),
182
- alt.Tooltip("date:T", title="Date"),
183
- ],
184
  )
185
  .add_params(brush)
186
  )
187
 
188
- map_layer = (background + geo_points).project(type="mercator").properties(
189
- width=420, height=450,
190
- title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)",
191
- )
192
-
193
- # Bar chart - full df
194
- type_chart = (
195
  alt.Chart(df)
196
  .mark_bar()
197
  .encode(
198
- x=alt.X("count():Q", title="Number of Crimes"),
199
- y=alt.Y("Primary Type:N", sort="-x", title="Crime Type"),
200
- color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray")),
201
- tooltip=["Primary Type:N", "count():Q"],
202
  )
203
- .properties(width=300, height=450, title="Crime Types (full dataset)")
204
  .add_params(click_type)
205
  .transform_filter(brush)
206
- .transform_filter(click_dist)
207
- )
208
-
209
- # Line chart - full df
210
- period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
211
- "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
212
- period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
213
-
214
- period_lines = (
215
- alt.Chart(df)
216
- .mark_line(point=False, strokeWidth=1.5)
217
- .encode(
218
- x=alt.X("Date_Only:T", title="Timeline"),
219
- y=alt.Y("count:Q", title="Number of Incidents", scale=alt.Scale(zero=True)),
220
- color=alt.Color(
221
- "Period:N",
222
- scale=alt.Scale(domain=period_order, range=period_range),
223
- legend=alt.Legend(title="Time of Day", orient="right"),
224
- ),
225
- tooltip=[
226
- alt.Tooltip("Date_Only:T", title="Date"),
227
- alt.Tooltip("Period:N", title="Period"),
228
- alt.Tooltip("count:Q", title="Incidents"),
229
- ],
230
- )
231
- .transform_filter(brush)
232
- .transform_filter(click_type)
233
- .transform_filter(click_dist)
234
- .transform_aggregate(count="count()", groupby=["Date_Only", "Period"])
235
- .transform_impute(impute="count", key="Date_Only", groupby=["Period"], value=0)
236
  )
237
 
238
- total_line = (
239
  alt.Chart(df)
240
- .mark_line(opacity=0.5)
241
  .encode(
242
- x=alt.X("Date_Only:T"),
243
- y=alt.Y("count():Q"),
244
- color=alt.datum("Total Daily"),
245
- tooltip=[
246
- alt.Tooltip("Date_Only:T", title="Date"),
247
- alt.Tooltip("count():Q", title="Total Incidents"),
248
- ],
249
  )
250
  .transform_filter(brush)
251
- .transform_filter(click_type)
252
- .transform_filter(click_dist)
253
  )
254
 
255
- line_chart = (total_line + period_lines).properties(
256
- width=760, height=220,
257
- title="Daily Crime Trend by Time of Day (full dataset)",
258
- ).resolve_scale(color="shared")
259
-
260
- dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
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)
319
 
320
  # ---------------------------------------------------------------------------
321
- # SECTION 3 Poverty vs. Crime
322
  # ---------------------------------------------------------------------------
323
- st.markdown("---")
324
- st.header("Does Poverty Predict Crime?")
325
- st.markdown(
326
- """
327
- Socioeconomic inequality is one of the most studied predictors of crime at the
328
- neighborhood level. The choropleth map on the left shades each of Chicago's 77
329
- community areas by their poverty rate - darker orange means higher poverty -
330
- with a binned crime density heatmap overlaid. The heatmap uses the full dataset
331
- with no sampling: each cell's color reflects how many incidents fall in that
332
- geographic bin, giving a clear picture of crime hotspots.
333
-
334
- The scatter plot on the right makes the poverty-crime relationship explicit:
335
- each dot is one community area, and the dashed line is a statistical trend.
336
- There is a moderate positive correlation, though it is far from deterministic -
337
- policy, policing patterns, and reporting rates all play a role.
338
-
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
 
343
- col3, col4 = st.columns(2)
344
-
345
- with col3:
346
- if not df_socio.empty and community_geojson["features"]:
347
- poverty_map = (
348
- alt.Chart(communities)
349
- .mark_geoshape(stroke="white", strokeWidth=0.4)
350
- .transform_lookup(
351
- lookup="properties.area_num_1",
352
- from_=alt.LookupData(df_socio, "ca", ["poverty_rate", "community_area_name"]),
353
- )
354
- .encode(
355
- color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"),
356
- title="Poverty Rate (%)"),
357
- tooltip=[
358
- alt.Tooltip("properties.community:N", title="Community"),
359
- alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
360
- ],
361
- )
362
- .project(type="mercator")
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(df_geo)
378
- .transform_bin("lat_bin", "latitude", bin=alt.Bin(maxbins=70))
379
- .transform_bin("lon_bin", "longitude", bin=alt.Bin(maxbins=70))
380
- .transform_aggregate(
381
- incident_count="count()",
382
- groupby=["lat_bin", "lon_bin"]
383
- )
384
- .mark_rect(opacity=0.55)
385
- .encode(
386
- longitude="lon_bin:Q",
387
- latitude="lat_bin:Q",
388
- color=alt.Color(
389
- "incident_count:Q",
390
- scale=alt.Scale(scheme="blues"),
391
- title="Incident Count",
392
- legend=alt.Legend(title="Incidents"),
393
- ),
394
- tooltip=[
395
- alt.Tooltip("incident_count:Q", title="Total Incidents")
396
- ]
397
  )
398
- .project(type="mercator") # 🔥 MUST align with choropleth
399
  )
400
- else:
401
- st.info("Socioeconomic or boundary data unavailable.")
402
-
403
- with col4:
404
- if not df_socio.empty and df["community_area"].notna().any():
405
- df_crime_count = (
406
- df.dropna(subset=["community_area"])
407
- .groupby("community_area").size()
408
- .reset_index(name="crime_count")
409
- )
410
- df_crime_count["ca"] = (
411
- df_crime_count["community_area"].astype(float).astype(int).astype(str)
412
- )
413
-
414
- df_scatter = pd.merge(
415
- df_socio[["ca", "community_area_name", "poverty_rate"]],
416
- df_crime_count[["ca", "crime_count"]],
417
- on="ca", how="inner",
418
- )
419
-
420
- if len(df_scatter) > 5:
421
- sc = (
422
- alt.Chart(df_scatter)
423
- .mark_circle(size=80, opacity=0.75)
424
- .encode(
425
- x=alt.X("poverty_rate:Q", title="Poverty Rate (%)"),
426
- y=alt.Y("crime_count:Q", title="Crime Count (2026)"),
427
- color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"),
428
- legend=None),
429
- tooltip=[
430
- alt.Tooltip("community_area_name:N", title="Community"),
431
- alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
432
- alt.Tooltip("crime_count:Q", title="Crime Count"),
433
- ],
434
- )
435
- )
436
- reg = sc.transform_regression("poverty_rate", "crime_count").mark_line(
437
- color="gray", strokeDash=[4, 4], strokeWidth=1.5
438
- )
439
- st.altair_chart(
440
- (sc + reg).properties(
441
- width=360, height=440,
442
- title="Higher Poverty -> More Crimes? (each dot = one community area)",
443
- ),
444
- use_container_width=True,
445
- )
446
- else:
447
- st.info("Not enough community-level overlap to render scatter plot.")
448
- else:
449
- st.info("Community area data not available in this dataset sample.")
450
 
451
- # ---------------------------------------------------------------------------
452
- # Citations
453
- # ---------------------------------------------------------------------------
454
- st.markdown("---")
455
- st.header("Data Sources & Citations")
456
- st.markdown(
457
- """
458
- | Dataset | Source | Link |
459
- |---|---|---|
460
- | Chicago Crimes 2001-Present | City of Chicago Data Portal | [ijzp-q8t2](https://data.cityofchicago.org/Public-Safety/Crimes-2001-to-Present/ijzp-q8t2) |
461
- | 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) |
462
- | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
463
- | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
464
-
465
- All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
466
- """
467
- )
 
3
  import altair as alt
4
  import json
5
  import urllib.request
6
+ import os
7
 
8
  st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="🚨", layout="wide")
9
  st.title("Crimes in Chicago - 2026")
10
  st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
11
  st.markdown("---")
12
+
13
+ st.markdown("""
14
+ ## What Is This About?
15
+ Every day, hundreds of crime incidents are reported across Chicago's 77 community areas.
16
+ This dashboard explores geography, timing, and socioeconomic context behind crime patterns.
17
+ """)
 
 
 
 
 
 
 
 
18
 
19
  # ---------------------------------------------------------------------------
20
+ # DATA LOADING
21
  # ---------------------------------------------------------------------------
22
  @st.cache_data(show_spinner="Loading local Chicago crime data...")
23
  def load_crime_data():
24
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
25
+ file_path = os.path.join(BASE_DIR, "Crimes_-_2026_20260417.csv")
 
 
 
 
 
 
 
 
26
 
27
+ df = pd.read_csv(file_path)
28
+ df.columns = [c.lower().replace(" ", "_") for c in df.columns]
29
 
 
 
 
 
 
 
 
 
30
  df["date"] = pd.to_datetime(df["date"], errors="coerce")
31
 
32
  for col in ["latitude", "longitude"]:
33
+ df[col] = pd.to_numeric(df[col], errors="coerce")
34
 
35
  df = df.dropna(subset=["date"])
36
 
37
+ df["date_only"] = df["date"].dt.floor("D")
38
+ df["hour"] = df["date"].dt.hour
39
  df["weekday"] = df["date"].dt.day_name().str[:3]
40
 
41
+ df["primary_type"] = df["primary_type"].str.upper()
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ df["district"] = pd.to_numeric(df["district"], errors="coerce").fillna(-1).astype(int).astype(str)
44
+ df["district_str"] = df["district"]
45
 
46
  def get_period(h):
47
  if 6 < h <= 12:
48
+ return "Morning"
49
  elif 12 < h <= 18:
50
+ return "Afternoon"
51
  elif 18 < h <= 24:
52
+ return "Evening"
53
  else:
54
+ return "Late Night"
55
 
56
+ df["period"] = df["hour"].apply(get_period)
57
 
58
  return df
59
 
60
+
61
  @st.cache_data(show_spinner="Loading socioeconomic data...")
62
  def load_socio():
63
+ url = "https://data.cityofchicago.org/resource/kn9c-c2s2.json"
64
+ df = pd.read_json(url)
65
+
66
+ df = df.dropna(subset=["ca"])
67
+ df["ca"] = df["ca"].astype(int).astype(str)
68
+ df["poverty_rate"] = pd.to_numeric(df["percent_households_below_poverty"], errors="coerce")
69
+
70
+ return df
71
+
72
 
73
  @st.cache_data(show_spinner="Loading boundaries...")
74
  def load_geojson(url):
75
+ with urllib.request.urlopen(url) as r:
76
+ return json.loads(r.read())
77
+
78
+
79
+ district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson")
 
 
 
80
  community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson")
81
 
82
+ df = load_crime_data()
83
  df_socio = load_socio()
84
+
85
+ districts = alt.Data(values=district_geojson["features"])
86
  communities = alt.Data(values=community_geojson["features"])
87
 
88
  if df.empty:
89
+ st.error("No data loaded")
90
  st.stop()
91
 
92
+ df_geo = df.dropna(subset=["latitude", "longitude"])
93
+
94
+ st.info(f"Loaded {len(df):,} records")
95
 
96
  # ---------------------------------------------------------------------------
97
+ # SECTION 1 DASHBOARD
98
  # ---------------------------------------------------------------------------
 
99
  st.header("Interactive Crime Dashboard")
 
 
 
 
 
 
 
 
 
 
 
100
 
101
+ brush = alt.selection_interval()
102
+ click_type = alt.selection_point(fields=["primary_type"])
103
+ click_dist = alt.selection_point(fields=["district_str"])
 
 
104
 
105
+ sample_df = df_geo.sample(min(5000, len(df_geo)), random_state=42)
 
 
 
 
 
 
 
 
 
 
106
 
107
+ map_chart = (
108
+ alt.Chart(sample_df)
109
  .mark_circle(size=5)
110
  .encode(
111
  longitude="longitude:Q",
112
  latitude="latitude:Q",
113
+ color=alt.condition(click_dist, "district_str:N", alt.value("lightgray")),
114
+ tooltip=["primary_type", "district_str", "date"]
 
 
 
 
 
 
 
 
 
 
115
  )
116
  .add_params(brush)
117
  )
118
 
119
+ bar_chart = (
 
 
 
 
 
 
120
  alt.Chart(df)
121
  .mark_bar()
122
  .encode(
123
+ x="count()",
124
+ y=alt.Y("primary_type:N", sort="-x"),
125
+ color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray"))
 
126
  )
 
127
  .add_params(click_type)
128
  .transform_filter(brush)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  )
130
 
131
+ line_chart = (
132
  alt.Chart(df)
133
+ .mark_line()
134
  .encode(
135
+ x="date_only:T",
136
+ y="count()",
137
+ color="period:N"
 
 
 
 
138
  )
139
  .transform_filter(brush)
 
 
140
  )
141
 
142
+ dashboard = (map_chart | bar_chart) & line_chart
 
 
 
 
 
143
  st.altair_chart(dashboard, use_container_width=True)
144
 
145
  # ---------------------------------------------------------------------------
146
+ # SECTION 2 HEATMAP
147
  # ---------------------------------------------------------------------------
148
+ st.header("When Crimes Happen")
 
 
 
 
 
 
 
 
149
 
150
+ top_types = df["primary_type"].value_counts().head(10).index.tolist()
 
151
 
152
+ hm = (
153
+ df.groupby(["primary_type", "weekday", "hour"])
 
 
154
  .size()
155
+ .reset_index(name="count")
156
  )
157
 
158
+ dropdown = alt.binding_select(options=[None] + list(top_types))
159
+ select = alt.selection_point(fields=["primary_type"], bind=dropdown)
 
 
 
 
 
 
 
160
 
161
  heatmap = (
162
+ alt.Chart(hm)
163
  .mark_rect()
164
  .encode(
165
+ x="weekday:N",
166
+ y="hour:O",
167
+ color="sum(count):Q"
 
 
 
 
 
 
 
 
 
 
 
 
168
  )
169
+ .add_params(select)
170
+ .transform_filter(select)
171
  )
172
+
173
  st.altair_chart(heatmap, use_container_width=True)
174
 
175
  # ---------------------------------------------------------------------------
176
+ # SECTION 3 (REMOVED crime_density)
177
  # ---------------------------------------------------------------------------
178
+ st.header("Does Poverty Predict Crime")
179
+
180
+ poverty_map = (
181
+ alt.Chart(communities)
182
+ .mark_geoshape()
183
+ .transform_lookup(
184
+ lookup="properties.area_num_1",
185
+ from_=alt.LookupData(df_socio, "ca", ["poverty_rate"])
186
+ )
187
+ .encode(
188
+ color="poverty_rate:Q",
189
+ tooltip=["poverty_rate:Q"]
190
+ )
 
 
 
 
 
191
  )
192
 
193
+ st.altair_chart(poverty_map, use_container_width=True)
194
+
195
+ # scatter
196
+ crime_by_area = (
197
+ df.dropna(subset=["community_area"])
198
+ .groupby("community_area")
199
+ .size()
200
+ .reset_index(name="crime_count")
201
+ )
202
+
203
+ crime_by_area["ca"] = crime_by_area["community_area"].astype(int).astype(str)
204
+
205
+ merged = pd.merge(df_socio, crime_by_area, on="ca", how="inner")
206
+
207
+ scatter = (
208
+ alt.Chart(merged)
209
+ .mark_circle()
210
+ .encode(
211
+ x="poverty_rate",
212
+ y="crime_count",
213
+ tooltip=["community_area_name", "poverty_rate", "crime_count"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  )
 
215
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ st.altair_chart(scatter, use_container_width=True)