XinyiC11 commited on
Commit
4478280
·
verified ·
1 Parent(s): 6a99134

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +0 -460
src/streamlit_app.py CHANGED
@@ -1,460 +0,0 @@
1
- import streamlit as st
2
- 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
-
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
- """
15
- ## What Is This About?
16
-
17
- Every day, hundreds of crime incidents are reported across Chicago's 77 community areas.
18
- But where do they happen? At what time? And does poverty play a role?
19
-
20
- This interactive article walks you through 2026 Chicago crime data drawn directly from
21
- the [Chicago Data Portal](https://data.cityofchicago.org/) to help you explore the
22
- geography, timing, and social context of crime in one of America's largest cities.
23
-
24
- The dataset records every reported crime incident in 2026, including the exact location,
25
- date and time, crime type, and the police district that handled it. Each row is one
26
- reported incident. We also include community-level socioeconomic data to examine the
27
- relationship between poverty and crime rates across Chicago's neighborhoods.
28
- """
29
- )
30
-
31
- # ---------------------------------------------------------------------------
32
- # Data loading
33
- # ---------------------------------------------------------------------------
34
-
35
- @st.cache_data(show_spinner="Loading Chicago crime data (full dataset)...")
36
- def load_crime_data():
37
- """Fetch ALL 2026 records in batches. Bar/line/heatmap use the full df;
38
- only geo rendering on maps uses sampling."""
39
- all_chunks = []
40
- limit = 50000
41
- offset = 0
42
- while True:
43
- url = (
44
- "https://data.cityofchicago.org/resource/ijzp-q8t2.json"
45
- "?$where=year=2026"
46
- f"&$limit={limit}"
47
- f"&$offset={offset}"
48
- "&$order=date%20DESC"
49
- )
50
- try:
51
- chunk = pd.read_json(url)
52
- except Exception as e:
53
- st.error(f"Failed to load crime data at offset {offset}: {e}")
54
- break
55
- if chunk.empty:
56
- break
57
- all_chunks.append(chunk)
58
- if len(chunk) < limit:
59
- break
60
- offset += limit
61
-
62
- if not all_chunks:
63
- return pd.DataFrame()
64
-
65
- df = pd.concat(all_chunks, ignore_index=True)
66
-
67
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
68
- for col in ["latitude", "longitude"]:
69
- df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
70
- df = df.dropna(subset=["date"])
71
-
72
- df["Date_Only"] = df["date"].dt.floor("d")
73
- df["Hour"] = df["date"].dt.hour
74
- df["weekday"] = df["date"].dt.day_name().str[:3]
75
-
76
- df["Primary Type"] = df["primary_type"].str.upper() if "primary_type" in df.columns else "UNKNOWN"
77
-
78
- if "district" in df.columns:
79
- df["District_Str"] = (
80
- pd.to_numeric(df["district"], errors="coerce")
81
- .fillna(-1).astype(int).astype(str)
82
- )
83
- df["District"] = df["District_Str"]
84
- else:
85
- df["District_Str"] = df["District"] = "-1"
86
-
87
- if "community_area" not in df.columns:
88
- df["community_area"] = None
89
-
90
- def get_period(h):
91
- if 6 < h <= 12: return "Morning (6am-12pm)"
92
- elif 12 < h <= 18: return "Afternoon (12pm-6pm)"
93
- elif 18 < h <= 24: return "Evening (6pm-12am)"
94
- else: return "Late Night (12am-6am)"
95
-
96
- df["Period"] = df["Hour"].apply(get_period)
97
- return df
98
-
99
-
100
- @st.cache_data(show_spinner="Loading socioeconomic data...")
101
- def load_socio():
102
- try:
103
- df = pd.read_json("https://data.cityofchicago.org/resource/kn9c-c2s2.json")
104
- df = df.dropna(subset=["ca"])
105
- df["ca"] = df["ca"].astype(float).astype(int).astype(str)
106
- df["poverty_rate"] = pd.to_numeric(df["percent_households_below_poverty"], errors="coerce")
107
- return df
108
- except Exception as e:
109
- st.warning(f"Could not load socioeconomic data: {e}")
110
- return pd.DataFrame(columns=["ca", "community_area_name", "poverty_rate"])
111
-
112
-
113
- @st.cache_data(show_spinner="Loading boundaries...")
114
- def load_geojson(url):
115
- try:
116
- with urllib.request.urlopen(url) as r:
117
- return json.loads(r.read())
118
- except Exception as e:
119
- st.warning(f"Could not load GeoJSON: {e}")
120
- return {"features": []}
121
-
122
-
123
- district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson")
124
- community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson")
125
-
126
- df = load_crime_data()
127
- df_socio = load_socio()
128
- districts = alt.Data(values=district_geojson["features"])
129
- communities = alt.Data(values=community_geojson["features"])
130
-
131
- if df.empty:
132
- st.error("Crime data could not be loaded.")
133
- st.stop()
134
-
135
- df_geo = df.dropna(subset=["latitude", "longitude"]).copy()
136
- st.info(f"Loaded **{len(df):,}** crime records for 2026 ({len(df_geo):,} with coordinates).")
137
-
138
- # ---------------------------------------------------------------------------
139
- # SECTION 1 — Linked dashboard
140
- # ---------------------------------------------------------------------------
141
- st.markdown("---")
142
- st.header("Interactive Crime Dashboard")
143
- st.markdown(
144
- """
145
- This dashboard lets you explore Chicago crime data across three linked views.
146
- **Drag a box on the map** to select a geographic area, or **click a district boundary**
147
- to highlight it — both actions filter the bar chart on the right and the timeline below.
148
- You can also **click a crime category** in the bar chart to drill into its temporal trend.
149
-
150
- The bottom line chart breaks daily incident counts into four time-of-day periods
151
- (plus a grey total line). The bar chart and line chart use the **full dataset** with
152
- no sampling; only the map points are sampled to keep the browser responsive.
153
- """
154
- )
155
-
156
- brush = alt.selection_interval(name="brush")
157
- click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
158
- click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
159
-
160
- MAP_SAMPLE = 5000
161
- df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
162
-
163
- background = (
164
- alt.Chart(districts)
165
- .mark_geoshape(stroke="black", strokeWidth=0.6)
166
- .transform_calculate(District_Str="datum.properties.dist_num")
167
- .encode(
168
- color=alt.condition(click_dist, alt.value("white"), alt.value("grey")),
169
- opacity=alt.condition(click_dist, alt.value(0.5), alt.value(0.8)),
170
- tooltip=[alt.Tooltip("properties.dist_num:N", title="District")],
171
- )
172
- .add_params(click_dist)
173
- )
174
-
175
- geo_points = (
176
- alt.Chart(df_map_sample)
177
- .mark_circle(size=5)
178
- .encode(
179
- longitude="longitude:Q",
180
- latitude="latitude:Q",
181
- color=alt.condition(
182
- click_dist,
183
- alt.Color("District:N", scale=alt.Scale(scheme="tableau10"),
184
- legend=alt.Legend(title="District", orient="right")),
185
- alt.value("#e0dbd6"),
186
- ),
187
- opacity=alt.condition(click_dist, alt.value(0.6), alt.value(0.05)),
188
- tooltip=[
189
- alt.Tooltip("Primary Type:N", title="Crime Type"),
190
- alt.Tooltip("District:N", title="District"),
191
- alt.Tooltip("date:T", title="Date"),
192
- ],
193
- )
194
- .add_params(brush)
195
- )
196
-
197
- map_layer = (background + geo_points).project(type="mercator").properties(
198
- width=420, height=450,
199
- title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)",
200
- )
201
-
202
- # Bar chart - full df
203
- type_chart = (
204
- alt.Chart(df)
205
- .mark_bar()
206
- .encode(
207
- x=alt.X("count():Q", title="Number of Crimes"),
208
- y=alt.Y("Primary Type:N", sort="-x", title="Crime Type"),
209
- color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray")),
210
- tooltip=["Primary Type:N", "count():Q"],
211
- )
212
- .properties(width=300, height=450, title="Crime Types (full dataset)")
213
- .add_params(click_type)
214
- .transform_filter(brush)
215
- .transform_filter(click_dist)
216
- )
217
-
218
- # Line chart - full df
219
- period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
220
- "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
221
- period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
222
-
223
- period_lines = (
224
- alt.Chart(df)
225
- .mark_line(point=False, strokeWidth=1.5)
226
- .encode(
227
- x=alt.X("Date_Only:T", title="Timeline"),
228
- y=alt.Y("count:Q", title="Number of Incidents", scale=alt.Scale(zero=True)),
229
- color=alt.Color(
230
- "Period:N",
231
- scale=alt.Scale(domain=period_order, range=period_range),
232
- legend=alt.Legend(title="Time of Day", orient="right"),
233
- ),
234
- tooltip=[
235
- alt.Tooltip("Date_Only:T", title="Date"),
236
- alt.Tooltip("Period:N", title="Period"),
237
- alt.Tooltip("count:Q", title="Incidents"),
238
- ],
239
- )
240
- .transform_filter(brush)
241
- .transform_filter(click_type)
242
- .transform_filter(click_dist)
243
- .transform_aggregate(count="count()", groupby=["Date_Only", "Period"])
244
- .transform_impute(impute="count", key="Date_Only", groupby=["Period"], value=0)
245
- )
246
-
247
- total_line = (
248
- alt.Chart(df)
249
- .mark_line(opacity=0.5)
250
- .encode(
251
- x=alt.X("Date_Only:T"),
252
- y=alt.Y("count():Q"),
253
- color=alt.datum("Total Daily"),
254
- tooltip=[
255
- alt.Tooltip("Date_Only:T", title="Date"),
256
- alt.Tooltip("count():Q", title="Total Incidents"),
257
- ],
258
- )
259
- .transform_filter(brush)
260
- .transform_filter(click_type)
261
- .transform_filter(click_dist)
262
- )
263
-
264
- line_chart = (total_line + period_lines).properties(
265
- width=760, height=220,
266
- title="Daily Crime Trend by Time of Day (full dataset)",
267
- ).resolve_scale(color="shared")
268
-
269
- dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
270
- st.altair_chart(dashboard, use_container_width=True)
271
-
272
- # ---------------------------------------------------------------------------
273
- # SECTION 2 — When do crimes happen? heatmap + dropdown
274
- # Pre-aggregate to 7x24 = 168 rows in Python before rendering,
275
- # so switching crime types is instant - no re-streaming of raw data to browser.
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
-
286
- Across nearly every category, Friday and Saturday evenings (6 pm to midnight)
287
- stand out as the most active windows, while the early-morning hours (2 to 5 am)
288
- are consistently quietest.
289
- """
290
- )
291
-
292
- top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
293
- selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
294
-
295
- # Key fix: groupby in Python first -> only 168 rows reach Altair -> instant render
296
- weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
297
- hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
298
- hm_agg = (
299
- hm_source
300
- .groupby(["weekday", "Hour"])
301
- .size()
302
- .reset_index(name="crime_count")
303
- )
304
-
305
- heatmap = (
306
- alt.Chart(hm_agg)
307
- .mark_rect()
308
- .encode(
309
- x=alt.X("weekday:N", sort=weekday_order, title="Day of Week"),
310
- y=alt.Y("Hour:O", title="Hour of Day (0-23)", sort="ascending"),
311
- color=alt.Color("crime_count:Q", scale=alt.Scale(scheme="reds"), title="Number of Crimes"),
312
- tooltip=[
313
- alt.Tooltip("weekday:N", title="Day"),
314
- alt.Tooltip("Hour:O", title="Hour"),
315
- alt.Tooltip("crime_count:Q", title="Total Crimes"),
316
- ],
317
- )
318
- .properties(
319
- width=700, height=380,
320
- title=f"Crime Heatmap - {selected_hm} (full dataset, darker = more incidents)",
321
- )
322
- )
323
- st.altair_chart(heatmap, use_container_width=True)
324
-
325
- # ---------------------------------------------------------------------------
326
- # SECTION 3 — Poverty vs. Crime
327
- # Left: choropleth + binned geo-heatmap (no sampling, full density visible)
328
- # Right: scatter with full crime counts per community area
329
- # ---------------------------------------------------------------------------
330
- st.markdown("---")
331
- st.header("Does Poverty Predict Crime?")
332
- st.markdown(
333
- """
334
- Socioeconomic inequality is one of the most studied predictors of crime at the
335
- neighborhood level. The choropleth map on the left shades each of Chicago's 77
336
- community areas by their poverty rate - darker orange means higher poverty -
337
- with a binned crime density heatmap overlaid. The heatmap uses the full dataset
338
- with no sampling: each cell's color reflects how many incidents fall in that
339
- geographic bin, giving a clear picture of crime hotspots.
340
-
341
- The scatter plot on the right makes the poverty-crime relationship explicit:
342
- each dot is one community area, and the dashed line is a statistical trend.
343
- There is a moderate positive correlation, though it is far from deterministic -
344
- policy, policing patterns, and reporting rates all play a role.
345
-
346
- **Socioeconomic data source:** [Census Data - Chicago Data Portal](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2)
347
- """
348
- )
349
-
350
- col3, col4 = st.columns(2)
351
-
352
- with col3:
353
- if not df_socio.empty and community_geojson["features"]:
354
- poverty_map = (
355
- alt.Chart(communities)
356
- .mark_geoshape(stroke="white", strokeWidth=0.4)
357
- .transform_lookup(
358
- lookup="properties.area_num_1",
359
- from_=alt.LookupData(df_socio, "ca", ["poverty_rate", "community_area_name"]),
360
- )
361
- .encode(
362
- color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"),
363
- title="Poverty Rate (%)"),
364
- tooltip=[
365
- alt.Tooltip("properties.community:N", title="Community"),
366
- alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
367
- ],
368
- )
369
- .project(type="mercator")
370
- .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
371
- )
372
-
373
- # Binned geo-heatmap: full dataset, no sampling needed
374
- # maxbins=50 -> ~2500 cells max, renders fast and shows full density
375
- crime_density = (
376
- alt.Chart(df_geo)
377
- .mark_rect(opacity=0.55)
378
- .encode(
379
- longitude=alt.X("longitude:Q", bin=alt.Bin(maxbins=50)),
380
- latitude=alt.Y("latitude:Q", bin=alt.Bin(maxbins=50)),
381
- color=alt.Color(
382
- "count():Q",
383
- scale=alt.Scale(scheme="blues"),
384
- title="Incident Count",
385
- legend=alt.Legend(title="Incidents"),
386
- ),
387
- )
388
- )
389
-
390
- st.altair_chart(
391
- (poverty_map + crime_density).resolve_scale(color="independent"),
392
- use_container_width=True,
393
- )
394
- else:
395
- st.info("Socioeconomic or boundary data unavailable.")
396
-
397
- with col4:
398
- if not df_socio.empty and df["community_area"].notna().any():
399
- # Full df for crime counts - no sampling
400
- df_crime_count = (
401
- df.dropna(subset=["community_area"])
402
- .groupby("community_area").size()
403
- .reset_index(name="crime_count")
404
- )
405
- df_crime_count["ca"] = (
406
- df_crime_count["community_area"].astype(float).astype(int).astype(str)
407
- )
408
- df_scatter = pd.merge(
409
- df_socio[["ca", "community_area_name", "poverty_rate"]],
410
- df_crime_count[["ca", "crime_count"]],
411
- on="ca", how="inner",
412
- )
413
- if len(df_scatter) > 5:
414
- sc = (
415
- alt.Chart(df_scatter)
416
- .mark_circle(size=80, opacity=0.75)
417
- .encode(
418
- x=alt.X("poverty_rate:Q", title="Poverty Rate (%)"),
419
- y=alt.Y("crime_count:Q", title="Crime Count (2026)"),
420
- color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"),
421
- legend=None),
422
- tooltip=[
423
- alt.Tooltip("community_area_name:N", title="Community"),
424
- alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
425
- alt.Tooltip("crime_count:Q", title="Crime Count"),
426
- ],
427
- )
428
- )
429
- reg = sc.transform_regression("poverty_rate", "crime_count").mark_line(
430
- color="gray", strokeDash=[4, 4], strokeWidth=1.5
431
- )
432
- st.altair_chart(
433
- (sc + reg).properties(
434
- width=360, height=440,
435
- title="Higher Poverty -> More Crimes? (each dot = one community area)",
436
- ),
437
- use_container_width=True,
438
- )
439
- else:
440
- st.info("Not enough community-level overlap to render scatter plot.")
441
- else:
442
- st.info("Community area data not available in this dataset sample.")
443
-
444
- # ---------------------------------------------------------------------------
445
- # Citations
446
- # ---------------------------------------------------------------------------
447
- st.markdown("---")
448
- st.header("Data Sources & Citations")
449
- st.markdown(
450
- """
451
- | Dataset | Source | Link |
452
- |---|---|---|
453
- | Chicago Crimes 2001-Present | City of Chicago Data Portal | [ijzp-q8t2](https://data.cityofchicago.org/Public-Safety/Crimes-2001-to-Present/ijzp-q8t2) |
454
- | 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) |
455
- | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
456
- | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
457
-
458
- All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
459
- """
460
- )