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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +61 -33
src/streamlit_app.py CHANGED
@@ -36,24 +36,45 @@ st.markdown(
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
@@ -77,7 +98,6 @@ def load_crime_data():
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)"
@@ -129,8 +149,13 @@ 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")
@@ -142,17 +167,19 @@ st.markdown(
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)
@@ -166,7 +193,7 @@ background = (
166
  )
167
 
168
  geo_points = (
169
- alt.Chart(df)
170
  .mark_circle(size=5)
171
  .encode(
172
  longitude="longitude:Q",
@@ -192,10 +219,10 @@ geo_points = (
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()
@@ -205,13 +232,13 @@ type_chart = (
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)",
@@ -264,10 +291,9 @@ total_line = (
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
 
@@ -279,13 +305,13 @@ 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
 
@@ -320,7 +346,7 @@ heatmap = (
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
  )
@@ -375,8 +401,9 @@ with col3:
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
  )
@@ -386,6 +413,7 @@ with col3:
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()
@@ -426,7 +454,7 @@ with col4:
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
  ),
 
36
  )
37
 
38
  # ── Data loading ──────────────────────────────────────────────────────────────
39
+ @st.cache_data(show_spinner="Loading Chicago crime data (full dataset)…")
40
  def load_crime_data():
41
+ """
42
+ Fetch ALL 2026 records in batches of 50 000.
43
+ Bar charts, line charts, and heatmaps use the full DataFrame.
44
+ Only map geo-points are sampled at render time.
45
+ """
46
+ all_chunks = []
47
+ limit = 50000
48
+ offset = 0
49
+ while True:
50
+ url = (
51
+ "https://data.cityofchicago.org/resource/ijzp-q8t2.json"
52
+ "?$where=year=2026"
53
+ f"&$limit={limit}"
54
+ f"&$offset={offset}"
55
+ "&$order=date%20DESC"
56
+ )
57
+ try:
58
+ chunk = pd.read_json(url)
59
+ except Exception as e:
60
+ st.error(f"Failed to load crime data at offset {offset}: {e}")
61
+ break
62
+ if chunk.empty:
63
+ break
64
+ all_chunks.append(chunk)
65
+ if len(chunk) < limit:
66
+ break
67
+ offset += limit
68
+
69
+ if not all_chunks:
70
  return pd.DataFrame()
71
 
72
+ df = pd.concat(all_chunks, ignore_index=True)
73
+
74
  df["date"] = pd.to_datetime(df["date"], errors="coerce")
75
  for col in ["latitude", "longitude"]:
76
  df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce")
77
+ df = df.dropna(subset=["date"]) # keep rows even if coords missing; drop only for map
78
 
79
  df["Date_Only"] = df["date"].dt.floor("d")
80
  df["Hour"] = df["date"].dt.hour
 
98
  if "community_area" not in df.columns:
99
  df["community_area"] = None
100
 
 
101
  def get_period(hour):
102
  if 6 < hour <= 12:
103
  return "Morning (6am-12pm)"
 
149
  st.error("⚠️ Crime data could not be loaded. Please check the Chicago Data Portal.")
150
  st.stop()
151
 
152
+ # Geo-only subset for map points (needs valid coords)
153
+ df_geo = df.dropna(subset=["latitude", "longitude"]).copy()
154
+
155
+ st.info(f"βœ… Loaded **{len(df):,}** crime records for 2026 ({len(df_geo):,} with coordinates).")
156
+
157
  # ═══════════════════════════════════════════════════════════════════════════════
158
+ # SECTION 1 β€” Linked dashboard
159
  # ═══════════════════════════════════════════════════════════════════════════════
160
  st.markdown("---")
161
  st.header("πŸ—ΊοΈ Interactive Crime Dashboard")
 
167
  You can also **click a crime category** in the bar chart to drill into its temporal trend.
168
 
169
  The bottom line chart breaks daily incident counts into four time-of-day periods
170
+ (plus a grey total line), so you can see not just *where* crime happens but *when* it peaks.
171
+ The bar chart and line chart use the **full dataset** β€” no sampling.
172
  """
173
  )
174
 
 
 
175
  brush = alt.selection_interval(name="brush")
176
  click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
177
  click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
178
 
179
+ # Map: geo points sampled to keep browser responsive; bar + line use full df
180
+ MAP_SAMPLE = 5000
181
+ df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
182
+
183
  background = (
184
  alt.Chart(districts)
185
  .mark_geoshape(stroke="black", strokeWidth=0.6)
 
193
  )
194
 
195
  geo_points = (
196
+ alt.Chart(df_map_sample)
197
  .mark_circle(size=5)
198
  .encode(
199
  longitude="longitude:Q",
 
219
 
220
  map_layer = (background + geo_points).project(type="mercator").properties(
221
  width=420, height=450,
222
+ title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)",
223
  )
224
 
225
+ # Bar chart β€” FULL df, filtered by brush + district selections
226
  type_chart = (
227
  alt.Chart(df)
228
  .mark_bar()
 
232
  color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray")),
233
  tooltip=["Primary Type:N", "count():Q"],
234
  )
235
+ .properties(width=300, height=450, title="Crime Types (full dataset)")
236
  .add_params(click_type)
237
  .transform_filter(brush)
238
  .transform_filter(click_dist)
239
  )
240
 
241
+ # Line chart β€” FULL df, filtered by all three selections
242
  period_order = [
243
  "Morning (6am-12pm)",
244
  "Afternoon (12pm-6pm)",
 
291
 
292
  line_chart = (total_line + period_lines).properties(
293
  width=760, height=220,
294
+ title="Daily Crime Trend by Time of Day (full dataset)",
295
  ).resolve_scale(color="shared")
296
 
 
297
  dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
298
  st.altair_chart(dashboard, use_container_width=True)
299
 
 
305
  st.markdown(
306
  """
307
  Different crimes follow different schedules. Use the **dropdown below** to filter
308
+ the heatmap to a specific crime category β€” or leave it on *All* to see the overall
309
+ pattern. Each cell shows the total number of incidents at that day-of-week Γ— hour-of-day
310
+ combination across the **full dataset**; darker red means more incidents.
311
 
312
  Across nearly every category, Friday and Saturday evenings (6 pm – midnight)
313
+ stand out as the most active windows, while the early-morning hours (2–5 am)
314
+ are consistently quietest.
315
  """
316
  )
317
 
 
346
  width=700, height=380,
347
  title=alt.TitleParams(
348
  text=f"Crime Heatmap β€” {selected_hm}",
349
+ subtitle="Select a crime type above to filter Β· Full dataset Β· Darker = more incidents",
350
  fontSize=14,
351
  ),
352
  )
 
401
  .project(type="mercator")
402
  .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
403
  )
404
+ # Overlay: sample only for rendering dots on the map (visual only, not analysis)
405
  crime_overlay = (
406
+ alt.Chart(df_geo.sample(min(5000, len(df_geo)), random_state=42))
407
  .mark_circle(size=3, color="steelblue", opacity=0.3)
408
  .encode(longitude="longitude:Q", latitude="latitude:Q")
409
  )
 
413
 
414
  with col4:
415
  if not df_socio.empty and df["community_area"].notna().any():
416
+ # Crime count by community uses the FULL df β€” no sampling
417
  df_crime_count = (
418
  df.dropna(subset=["community_area"])
419
  .groupby("community_area").size()
 
454
  width=360, height=440,
455
  title=alt.TitleParams(
456
  text="Higher Poverty β†’ More Crimes?",
457
+ subtitle="Each dot = one community area | Dashed = trend | Full dataset counts",
458
  fontSize=13,
459
  ),
460
  ),