XinyiC11 commited on
Commit
b475c5c
Β·
verified Β·
1 Parent(s): ad2d75c

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +444 -30
src/streamlit_app.py CHANGED
@@ -1,40 +1,454 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
 
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
 
 
 
 
 
 
 
 
18
 
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
 
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
 
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
 
35
  .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import altair as alt
4
+ import json
5
+ import urllib.request
6
+
7
+ # ── Page config ───────────────────────────────────────────────────────────────
8
+ st.set_page_config(
9
+ page_title="Crimes in Chicago 2026",
10
+ page_icon="πŸ”",
11
+ layout="wide",
12
+ )
13
+
14
+ # ── Header ────────────────────────────────────────────────────────────────────
15
+ st.title("πŸ” Crimes in Chicago β€” 2026")
16
+ st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** Β· Group 6")
17
+ st.markdown("---")
18
+
19
+ # ── Introduction ──────────────────────────────────────────────────────────────
20
+ st.markdown(
21
+ """
22
+ ## What Is This About?
23
+
24
+ Every day, hundreds of crime incidents are reported across Chicago's 77 community areas.
25
+ But where do they happen? At what time? And does poverty play a role?
26
+
27
+ This interactive article walks you through 2026 Chicago crime data β€” drawn directly from
28
+ the [Chicago Data Portal](https://data.cityofchicago.org/) β€” to help you explore the
29
+ geography, timing, and social context of crime in one of America's largest cities.
30
+
31
+ The dataset records every reported crime incident in 2026, including the exact location,
32
+ date and time, crime type, and the police district that handled it. Each row is one
33
+ reported incident. We also include community-level socioeconomic data to examine the
34
+ relationship between poverty and crime rates across Chicago's neighborhoods.
35
+ """
36
+ )
37
+
38
+ # ── Data loading ──────────────────────────────────────────────────────────────
39
+ @st.cache_data(show_spinner="Loading Chicago crime data…")
40
+ def load_crime_data():
41
+ url = (
42
+ "https://data.cityofchicago.org/resource/ijzp-q8t2.json"
43
+ "?$where=year=2026"
44
+ "&$limit=10000"
45
+ "&$order=date%20DESC"
46
+ )
47
+ try:
48
+ df = pd.read_json(url)
49
+ except Exception as e:
50
+ st.error(f"Failed to load crime data: {e}")
51
+ return pd.DataFrame()
52
+
53
+ df["date"] = pd.to_datetime(df["date"], errors="coerce")
54
+ for col in ["latitude", "longitude"]:
55
+ df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
56
+ df = df.dropna(subset=["latitude", "longitude", "date"])
57
+
58
+ df["Date_Only"] = df["date"].dt.floor("d")
59
+ df["Hour"] = df["date"].dt.hour
60
+ df["weekday"] = df["date"].dt.day_name().str[:3]
61
 
62
+ if "primary_type" in df.columns:
63
+ df["Primary Type"] = df["primary_type"].str.upper()
64
+ else:
65
+ df["Primary Type"] = "UNKNOWN"
66
 
67
+ if "district" in df.columns:
68
+ df["District_Str"] = (
69
+ pd.to_numeric(df["district"], errors="coerce")
70
+ .fillna(-1).astype(int).astype(str)
71
+ )
72
+ df["District"] = df["District_Str"]
73
+ else:
74
+ df["District_Str"] = "-1"
75
+ df["District"] = "-1"
76
 
77
+ if "community_area" not in df.columns:
78
+ df["community_area"] = None
79
 
80
+ # Period column
81
+ def get_period(hour):
82
+ if 6 < hour <= 12:
83
+ return "Morning (6am-12pm)"
84
+ elif 12 < hour <= 18:
85
+ return "Afternoon (12pm-6pm)"
86
+ elif 18 < hour <= 24:
87
+ return "Evening (6pm-12am)"
88
+ else:
89
+ return "Late Night (12am-6am)"
90
 
91
+ df["Period"] = df["Hour"].apply(get_period)
92
+ return df
 
93
 
 
 
94
 
95
+ @st.cache_data(show_spinner="Loading socioeconomic data…")
96
+ def load_socio():
97
+ url = "https://data.cityofchicago.org/resource/kn9c-c2s2.json"
98
+ try:
99
+ df = pd.read_json(url)
100
+ df = df.dropna(subset=["ca"])
101
+ df["ca"] = df["ca"].astype(float).astype(int).astype(str)
102
+ df["poverty_rate"] = pd.to_numeric(df["percent_households_below_poverty"], errors="coerce")
103
+ return df
104
+ except Exception as e:
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:
112
+ with urllib.request.urlopen(url) as r:
113
+ return json.loads(r.read())
114
+ except Exception as e:
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
+
122
+ df = load_crime_data()
123
+ df_socio = load_socio()
124
+
125
+ districts = alt.Data(values=district_geojson["features"])
126
+ communities = alt.Data(values=community_geojson["features"])
127
+
128
+ if df.empty:
129
+ st.error("⚠️ Crime data could not be loaded. Please check the Chicago Data Portal.")
130
+ st.stop()
131
+
132
+ # ═══════════════════════════════════════════════════════════════════════════════
133
+ # SECTION 1 β€” Linked dashboard (map | bar chart) & time-of-day line chart
134
+ # ═══════════════════════════════════════════════════════════════════════════════
135
+ st.markdown("---")
136
+ st.header("πŸ—ΊοΈ Interactive Crime Dashboard")
137
+ st.markdown(
138
+ """
139
+ This dashboard lets you explore Chicago crime data across three linked views.
140
+ **Drag a box on the map** to select a geographic area, or **click a district boundary**
141
+ to highlight it β€” both actions filter the bar chart on the right and the timeline below.
142
+ You can also **click a crime category** in the bar chart to drill into its temporal trend.
143
+
144
+ The bottom line chart breaks daily incident counts into four time-of-day periods
145
+ (plus a total), so you can see not just *where* crime happens but *when* it peaks.
146
+ """
147
+ )
148
+
149
+ # Altair selections (note: cross-chart filtering via selections only works when
150
+ # the entire compound chart is rendered as one Altair object, which st.altair_chart supports)
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
+ # ── Map layer ─────────────────────────────────────────────────────────────────
156
+ background = (
157
+ alt.Chart(districts)
158
+ .mark_geoshape(stroke="black", strokeWidth=0.6)
159
+ .transform_calculate(District_Str="datum.properties.dist_num")
160
+ .encode(
161
+ color=alt.condition(click_dist, alt.value("white"), alt.value("grey")),
162
+ opacity=alt.condition(click_dist, alt.value(0.5), alt.value(0.8)),
163
+ tooltip=[alt.Tooltip("properties.dist_num:N", title="District")],
164
+ )
165
+ .add_params(click_dist)
166
+ )
167
 
168
+ geo_points = (
169
+ alt.Chart(df)
170
+ .mark_circle(size=5)
171
  .encode(
172
+ longitude="longitude:Q",
173
+ latitude="latitude:Q",
174
+ color=alt.condition(
175
+ click_dist,
176
+ alt.Color(
177
+ "District:N",
178
+ scale=alt.Scale(scheme="tableau10"),
179
+ legend=alt.Legend(title="District", orient="right"),
180
+ ),
181
+ alt.value("#e0dbd6"),
182
+ ),
183
+ opacity=alt.condition(click_dist, alt.value(0.6), alt.value(0.05)),
184
+ tooltip=[
185
+ alt.Tooltip("Primary Type:N", title="Crime Type"),
186
+ alt.Tooltip("District:N", title="District"),
187
+ alt.Tooltip("date:T", title="Date"),
188
+ ],
189
+ )
190
+ .add_params(brush)
191
+ )
192
+
193
+ map_layer = (background + geo_points).project(type="mercator").properties(
194
+ width=420, height=450,
195
+ title="Chicago Crime Map (Brush to select area / Click district)",
196
+ )
197
+
198
+ # ── Crime-type bar chart ──────────────────────────────────────────────────────
199
+ type_chart = (
200
+ alt.Chart(df)
201
+ .mark_bar()
202
+ .encode(
203
+ x=alt.X("count():Q", title="Number of Crimes"),
204
+ y=alt.Y("Primary Type:N", sort="-x", title="Crime Type"),
205
+ color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray")),
206
+ tooltip=["Primary Type:N", "count():Q"],
207
+ )
208
+ .properties(width=300, height=450, title="Crime Types")
209
+ .add_params(click_type)
210
+ .transform_filter(brush)
211
+ .transform_filter(click_dist)
212
+ )
213
+
214
+ # ── Time-of-day line chart ────────────────────────────────────────────────────
215
+ period_order = [
216
+ "Morning (6am-12pm)",
217
+ "Afternoon (12pm-6pm)",
218
+ "Evening (6pm-12am)",
219
+ "Late Night (12am-6am)",
220
+ "Total Daily",
221
+ ]
222
+ period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
223
+
224
+ period_lines = (
225
+ alt.Chart(df)
226
+ .mark_line(point=False, strokeWidth=1.5)
227
+ .encode(
228
+ x=alt.X("Date_Only:T", title="Timeline"),
229
+ y=alt.Y("count:Q", title="Number of Incidents", scale=alt.Scale(zero=True)),
230
+ color=alt.Color(
231
+ "Period:N",
232
+ scale=alt.Scale(domain=period_order, range=period_range),
233
+ legend=alt.Legend(title="Time of Day", orient="right"),
234
+ ),
235
+ tooltip=[
236
+ alt.Tooltip("Date_Only:T", title="Date"),
237
+ alt.Tooltip("Period:N", title="Period"),
238
+ alt.Tooltip("count:Q", title="Incidents"),
239
+ ],
240
+ )
241
+ .transform_filter(brush)
242
+ .transform_filter(click_type)
243
+ .transform_filter(click_dist)
244
+ .transform_aggregate(count="count()", groupby=["Date_Only", "Period"])
245
+ .transform_impute(impute="count", key="Date_Only", groupby=["Period"], value=0)
246
+ )
247
+
248
+ total_line = (
249
+ alt.Chart(df)
250
+ .mark_line(opacity=0.5)
251
+ .encode(
252
+ x=alt.X("Date_Only:T"),
253
+ y=alt.Y("count():Q"),
254
+ color=alt.datum("Total Daily"),
255
+ tooltip=[
256
+ alt.Tooltip("Date_Only:T", title="Date"),
257
+ alt.Tooltip("count():Q", title="Total Incidents"),
258
+ ],
259
+ )
260
+ .transform_filter(brush)
261
+ .transform_filter(click_type)
262
+ .transform_filter(click_dist)
263
+ )
264
+
265
+ line_chart = (total_line + period_lines).properties(
266
+ width=760, height=220,
267
+ title="Daily Crime Trend by Time of Day",
268
+ ).resolve_scale(color="shared")
269
+
270
+ # ── Compose full dashboard ────────────────────────────────────────────────────
271
+ dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
272
+ st.altair_chart(dashboard, use_container_width=True)
273
+
274
+ # ═══════════════════════════════════════════════════════════════════════════════
275
+ # SECTION 2 β€” When do crimes happen? (standalone 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 to a specific crime category β€” or leave it on *All* to see the
283
+ overall pattern. Each cell shows the total number of incidents at that
284
+ day-of-week Γ— hour-of-day combination; darker red means more incidents.
285
+
286
+ Across nearly every category, Friday and Saturday evenings (6 pm – midnight)
287
+ stand out as the most active windows, while the early morning hours (2–5 am)
288
+ are quietest β€” except for a few crime types that peak overnight.
289
+ """
290
+ )
291
+
292
+ top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
293
+ selected_hm = st.selectbox(
294
+ "Select Crime Type",
295
+ options=["All"] + top_types_hm,
296
+ index=0,
297
+ )
298
+
299
+ hm_df = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
300
+ weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
301
+
302
+ heatmap = (
303
+ alt.Chart(hm_df)
304
+ .mark_rect()
305
+ .encode(
306
+ x=alt.X("weekday:N", sort=weekday_order, title="Day of Week"),
307
+ y=alt.Y("Hour:O", title="Hour of Day (0–23)", sort="ascending"),
308
+ color=alt.Color(
309
+ "count():Q",
310
+ scale=alt.Scale(scheme="reds"),
311
+ title="Number of Crimes",
312
+ ),
313
+ tooltip=[
314
+ alt.Tooltip("weekday:N", title="Day"),
315
+ alt.Tooltip("Hour:O", title="Hour"),
316
+ alt.Tooltip("count():Q", title="Total Crimes"),
317
+ ],
318
+ )
319
+ .properties(
320
+ width=700, height=380,
321
+ title=alt.TitleParams(
322
+ text=f"Crime Heatmap β€” {selected_hm}",
323
+ subtitle="Select a crime type above to filter Β· Darker = more incidents",
324
+ fontSize=14,
325
+ ),
326
+ )
327
+ )
328
+ st.altair_chart(heatmap, use_container_width=True)
329
+
330
+ # ═══════════════════════════════════════════════════════════════════════════════
331
+ # SECTION 3 β€” Poverty vs. Crime
332
+ # ════════════════════════════════���══════════════════════════════════════════════
333
+ st.markdown("---")
334
+ st.header("πŸ’Έ Does Poverty Predict Crime?")
335
+ st.markdown(
336
+ """
337
+ Socioeconomic inequality is one of the most studied predictors of crime at the
338
+ neighborhood level. The choropleth map on the left shades each of Chicago's 77
339
+ community areas by their poverty rate β€” darker orange means higher poverty β€”
340
+ with crime incident dots overlaid in blue.
341
+
342
+ A visual comparison suggests that some of the highest-crime community areas,
343
+ particularly on the South and West sides, also carry the heaviest poverty burden.
344
+ The scatter plot on the right makes this relationship explicit: each dot is one
345
+ community area, and the dashed line is a statistical trend. There is a moderate
346
+ positive correlation, though it is far from deterministic β€” policy, policing
347
+ patterns, and reporting rates all play a role.
348
+
349
+ **Socioeconomic data source:** [Census Data β€” Chicago Data Portal](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2)
350
+ """
351
+ )
352
+
353
+ col3, col4 = st.columns(2)
354
+
355
+ with col3:
356
+ if not df_socio.empty and community_geojson["features"]:
357
+ poverty_map = (
358
+ alt.Chart(communities)
359
+ .mark_geoshape(stroke="white", strokeWidth=0.4)
360
+ .transform_lookup(
361
+ lookup="properties.area_num_1",
362
+ from_=alt.LookupData(df_socio, "ca", ["poverty_rate", "community_area_name"]),
363
+ )
364
+ .encode(
365
+ color=alt.Color(
366
+ "poverty_rate:Q",
367
+ scale=alt.Scale(scheme="orangered"),
368
+ title="Poverty Rate (%)",
369
+ ),
370
+ tooltip=[
371
+ alt.Tooltip("properties.community:N", title="Community"),
372
+ alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
373
+ ],
374
+ )
375
+ .project(type="mercator")
376
+ .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
377
+ )
378
+ crime_overlay = (
379
+ alt.Chart(df.sample(min(5000, len(df)), random_state=42))
380
+ .mark_circle(size=3, color="steelblue", opacity=0.3)
381
+ .encode(longitude="longitude:Q", latitude="latitude:Q")
382
+ )
383
+ st.altair_chart(poverty_map + crime_overlay, use_container_width=True)
384
+ else:
385
+ st.info("Socioeconomic or boundary data unavailable.")
386
+
387
+ with col4:
388
+ if not df_socio.empty and df["community_area"].notna().any():
389
+ df_crime_count = (
390
+ df.dropna(subset=["community_area"])
391
+ .groupby("community_area").size()
392
+ .reset_index(name="crime_count")
393
+ )
394
+ df_crime_count["ca"] = (
395
+ df_crime_count["community_area"].astype(float).astype(int).astype(str)
396
+ )
397
+ df_scatter = pd.merge(
398
+ df_socio[["ca", "community_area_name", "poverty_rate"]],
399
+ df_crime_count[["ca", "crime_count"]],
400
+ on="ca", how="inner",
401
+ )
402
+ if len(df_scatter) > 5:
403
+ sc = (
404
+ alt.Chart(df_scatter)
405
+ .mark_circle(size=80, opacity=0.75)
406
+ .encode(
407
+ x=alt.X("poverty_rate:Q", title="Poverty Rate (%)"),
408
+ y=alt.Y("crime_count:Q", title="Crime Count (2026)"),
409
+ color=alt.Color(
410
+ "poverty_rate:Q",
411
+ scale=alt.Scale(scheme="orangered"),
412
+ legend=None,
413
+ ),
414
+ tooltip=[
415
+ alt.Tooltip("community_area_name:N", title="Community"),
416
+ alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"),
417
+ alt.Tooltip("crime_count:Q", title="Crime Count"),
418
+ ],
419
+ )
420
+ )
421
+ reg = sc.transform_regression("poverty_rate", "crime_count").mark_line(
422
+ color="gray", strokeDash=[4, 4], strokeWidth=1.5
423
+ )
424
+ st.altair_chart(
425
+ (sc + reg).properties(
426
+ width=360, height=440,
427
+ title=alt.TitleParams(
428
+ text="Higher Poverty β†’ More Crimes?",
429
+ subtitle="Each dot = one community area | Dashed = trend",
430
+ fontSize=13,
431
+ ),
432
+ ),
433
+ use_container_width=True,
434
+ )
435
+ else:
436
+ st.info("Not enough community-level overlap to render scatter plot.")
437
+ else:
438
+ st.info("Community area data not available in this dataset sample.")
439
+
440
+ # ── Citations ─────────────────────────────────────────────────────────────────
441
+ st.markdown("---")
442
+ st.header("πŸ“š Data Sources & Citations")
443
+ st.markdown(
444
+ """
445
+ | Dataset | Source | Link |
446
+ |---|---|---|
447
+ | Chicago Crimes 2001–Present | City of Chicago Data Portal | [ijzp-q8t2](https://data.cityofchicago.org/Public-Safety/Crimes-2001-to-Present/ijzp-q8t2) |
448
+ | 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) |
449
+ | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
450
+ | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
451
+
452
+ All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
453
+ """
454
+ )