Update src/streamlit_app.py
Browse files- src/streamlit_app.py +59 -68
src/streamlit_app.py
CHANGED
|
@@ -4,7 +4,10 @@ import altair as alt
|
|
| 4 |
import json
|
| 5 |
import urllib.request
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
st.title("Crimes in Chicago - 2026")
|
| 10 |
st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
|
|
@@ -29,59 +32,45 @@ st.markdown(
|
|
| 29 |
# Data loading
|
| 30 |
# ---------------------------------------------------------------------------
|
| 31 |
|
| 32 |
-
@st.cache_data(show_spinner="Loading Chicago crime data
|
| 33 |
def load_crime_data():
|
| 34 |
-
"""
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
url = (
|
| 41 |
-
"https://data.cityofchicago.org/resource/ijzp-q8t2.json"
|
| 42 |
-
"?$where=year=2026"
|
| 43 |
-
f"&$limit={limit}"
|
| 44 |
-
f"&$offset={offset}"
|
| 45 |
-
"&$order=date%20DESC"
|
| 46 |
-
)
|
| 47 |
-
try:
|
| 48 |
-
chunk = pd.read_json(url)
|
| 49 |
-
except Exception as e:
|
| 50 |
-
st.error(f"Failed to load crime data at offset {offset}: {e}")
|
| 51 |
-
break
|
| 52 |
-
if chunk.empty:
|
| 53 |
-
break
|
| 54 |
-
all_chunks.append(chunk)
|
| 55 |
-
if len(chunk) < limit:
|
| 56 |
-
break
|
| 57 |
-
offset += limit
|
| 58 |
-
|
| 59 |
-
if not all_chunks:
|
| 60 |
return pd.DataFrame()
|
| 61 |
|
| 62 |
-
df =
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
| 67 |
df = df.dropna(subset=["date"])
|
| 68 |
|
| 69 |
-
|
|
|
|
| 70 |
df["Hour"] = df["date"].dt.hour
|
| 71 |
df["weekday"] = df["date"].dt.day_name().str[:3]
|
| 72 |
|
| 73 |
-
df["Primary Type"] = df["
|
| 74 |
|
| 75 |
-
if "
|
| 76 |
df["District_Str"] = (
|
| 77 |
-
pd.to_numeric(df["
|
| 78 |
.fillna(-1).astype(int).astype(str)
|
| 79 |
)
|
| 80 |
df["District"] = df["District_Str"]
|
| 81 |
else:
|
| 82 |
df["District_Str"] = df["District"] = "-1"
|
| 83 |
|
| 84 |
-
if "
|
|
|
|
|
|
|
| 85 |
df["community_area"] = None
|
| 86 |
|
| 87 |
def get_period(h):
|
|
@@ -153,6 +142,7 @@ brush = alt.selection_interval(name="brush")
|
|
| 153 |
click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
|
| 154 |
click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
|
| 155 |
|
|
|
|
| 156 |
MAP_SAMPLE = 5000
|
| 157 |
df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
|
| 158 |
|
|
@@ -192,10 +182,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 -
|
| 199 |
type_chart = (
|
| 200 |
alt.Chart(df)
|
| 201 |
.mark_bar()
|
|
@@ -211,7 +201,7 @@ type_chart = (
|
|
| 211 |
.transform_filter(click_dist)
|
| 212 |
)
|
| 213 |
|
| 214 |
-
# Line chart -
|
| 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,12 +253,12 @@ 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 |
st.altair_chart(dashboard, use_container_width=True)
|
| 267 |
|
| 268 |
# ---------------------------------------------------------------------------
|
| 269 |
# SECTION 2 — When do crimes happen? heatmap + dropdown
|
| 270 |
-
# Pre-aggregate to 7x24 = 168 rows in Python before rendering,
|
| 271 |
-
# so switching crime types is instant - no re-streaming of raw data to browser.
|
| 272 |
# ---------------------------------------------------------------------------
|
| 273 |
st.markdown("---")
|
| 274 |
st.header("When Do Crimes Happen in Chicago?")
|
|
@@ -278,16 +268,12 @@ 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 |
-
Across nearly every category, Friday and Saturday evenings (6 pm to midnight)
|
| 282 |
-
stand out as the most active windows, while the early-morning hours (2 to 5 am)
|
| 283 |
-
are consistently quietest.
|
| 284 |
"""
|
| 285 |
)
|
| 286 |
|
| 287 |
top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
|
| 288 |
selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
|
| 289 |
|
| 290 |
-
# Key fix: groupby in Python first -> only 168 rows reach Altair -> instant render
|
| 291 |
weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
| 292 |
hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
|
| 293 |
hm_agg = (
|
|
@@ -312,15 +298,13 @@ heatmap = (
|
|
| 312 |
)
|
| 313 |
.properties(
|
| 314 |
width=700, height=380,
|
| 315 |
-
title=f"Crime Heatmap - {selected_hm} (full dataset
|
| 316 |
)
|
| 317 |
)
|
| 318 |
st.altair_chart(heatmap, use_container_width=True)
|
| 319 |
|
| 320 |
# ---------------------------------------------------------------------------
|
| 321 |
# SECTION 3 — Poverty vs. Crime
|
| 322 |
-
# Left: choropleth + binned geo-heatmap (no sampling, full density visible)
|
| 323 |
-
# Right: scatter with full crime counts per community area
|
| 324 |
# ---------------------------------------------------------------------------
|
| 325 |
st.markdown("---")
|
| 326 |
st.header("Does Poverty Predict Crime?")
|
|
@@ -328,15 +312,8 @@ st.markdown(
|
|
| 328 |
"""
|
| 329 |
Socioeconomic inequality is one of the most studied predictors of crime at the
|
| 330 |
neighborhood level. The choropleth map on the left shades each of Chicago's 77
|
| 331 |
-
community areas by their poverty rate
|
| 332 |
-
|
| 333 |
-
with no sampling: each cell's color reflects how many incidents fall in that
|
| 334 |
-
geographic bin, giving a clear picture of crime hotspots.
|
| 335 |
-
The scatter plot on the right makes the poverty-crime relationship explicit:
|
| 336 |
-
each dot is one community area, and the dashed line is a statistical trend.
|
| 337 |
-
There is a moderate positive correlation, though it is far from deterministic -
|
| 338 |
-
policy, policing patterns, and reporting rates all play a role.
|
| 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 |
|
|
@@ -363,20 +340,34 @@ with col3:
|
|
| 363 |
.properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
|
| 364 |
)
|
| 365 |
|
| 366 |
-
#
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
crime_density = (
|
| 369 |
-
alt.Chart(
|
| 370 |
-
.
|
| 371 |
.encode(
|
| 372 |
-
longitude=
|
| 373 |
-
latitude=
|
| 374 |
color=alt.Color(
|
| 375 |
-
"
|
| 376 |
scale=alt.Scale(scheme="blues"),
|
| 377 |
title="Incident Count",
|
| 378 |
legend=alt.Legend(title="Incidents"),
|
| 379 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
)
|
| 381 |
)
|
| 382 |
|
|
@@ -389,7 +380,6 @@ with col3:
|
|
| 389 |
|
| 390 |
with col4:
|
| 391 |
if not df_socio.empty and df["community_area"].notna().any():
|
| 392 |
-
# Full df for crime counts - no sampling
|
| 393 |
df_crime_count = (
|
| 394 |
df.dropna(subset=["community_area"])
|
| 395 |
.groupby("community_area").size()
|
|
@@ -425,7 +415,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 |
)
|
|
@@ -447,6 +437,7 @@ st.markdown(
|
|
| 447 |
| 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) |
|
| 448 |
| Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
|
| 449 |
| Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
|
|
|
|
| 450 |
All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
|
| 451 |
"""
|
| 452 |
)
|
|
|
|
| 4 |
import json
|
| 5 |
import urllib.request
|
| 6 |
|
| 7 |
+
# 🌟 关键:解除 Altair 5000 行的限制,允许柱状图和折线图使用全量数据渲染
|
| 8 |
+
alt.data_transformers.disable_max_rows()
|
| 9 |
+
|
| 10 |
+
st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="🚨", layout="wide")
|
| 11 |
|
| 12 |
st.title("Crimes in Chicago - 2026")
|
| 13 |
st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6")
|
|
|
|
| 32 |
# Data loading
|
| 33 |
# ---------------------------------------------------------------------------
|
| 34 |
|
| 35 |
+
@st.cache_data(show_spinner="Loading Chicago crime data from CSV...")
|
| 36 |
def load_crime_data():
|
| 37 |
+
"""直接读取上传到 Hugging Face 的 CSV 文件,不再使用 API 循环请求。"""
|
| 38 |
+
try:
|
| 39 |
+
# 读取本地上传的 CSV 文件
|
| 40 |
+
df_raw = pd.read_csv("Crimes_-_2026_20260417.csv", low_memory=False)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
st.error(f"Failed to load CSV file. 请确保文件名准确并已上传到 Hugging Face: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
return pd.DataFrame()
|
| 44 |
|
| 45 |
+
df = df_raw.copy()
|
| 46 |
+
|
| 47 |
+
# 适配 CSV 列名
|
| 48 |
+
df["date"] = pd.to_datetime(df["Date"], format='%m/%d/%Y %I:%M:%S %p', errors="coerce")
|
| 49 |
+
for col, orig_col in [("latitude", "Latitude"), ("longitude", "Longitude")]:
|
| 50 |
+
if orig_col in df.columns:
|
| 51 |
+
df[col] = pd.to_numeric(df[orig_col], errors="coerce")
|
| 52 |
+
|
| 53 |
df = df.dropna(subset=["date"])
|
| 54 |
|
| 55 |
+
# 🌟 修复 Pandas 'd' 为 'D' 弃用警告
|
| 56 |
+
df["Date_Only"] = df["date"].dt.floor("D")
|
| 57 |
df["Hour"] = df["date"].dt.hour
|
| 58 |
df["weekday"] = df["date"].dt.day_name().str[:3]
|
| 59 |
|
| 60 |
+
df["Primary Type"] = df["Primary Type"].str.upper() if "Primary Type" in df.columns else "UNKNOWN"
|
| 61 |
|
| 62 |
+
if "District" in df.columns:
|
| 63 |
df["District_Str"] = (
|
| 64 |
+
pd.to_numeric(df["District"], errors="coerce")
|
| 65 |
.fillna(-1).astype(int).astype(str)
|
| 66 |
)
|
| 67 |
df["District"] = df["District_Str"]
|
| 68 |
else:
|
| 69 |
df["District_Str"] = df["District"] = "-1"
|
| 70 |
|
| 71 |
+
if "Community Area" in df.columns:
|
| 72 |
+
df["community_area"] = df["Community Area"]
|
| 73 |
+
else:
|
| 74 |
df["community_area"] = None
|
| 75 |
|
| 76 |
def get_period(h):
|
|
|
|
| 142 |
click_type = alt.selection_point(fields=["Primary Type"], name="click_type")
|
| 143 |
click_dist = alt.selection_point(fields=["District_Str"], name="click_dist")
|
| 144 |
|
| 145 |
+
# 地图图层采样 (保护浏览器),但右侧柱状图/折线图坚持用全量数据 df
|
| 146 |
MAP_SAMPLE = 5000
|
| 147 |
df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42)
|
| 148 |
|
|
|
|
| 182 |
|
| 183 |
map_layer = (background + geo_points).project(type="mercator").properties(
|
| 184 |
width=420, height=450,
|
| 185 |
+
title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points)",
|
| 186 |
)
|
| 187 |
|
| 188 |
+
# 🌟 Bar chart - 坚持使用全量数据集 df
|
| 189 |
type_chart = (
|
| 190 |
alt.Chart(df)
|
| 191 |
.mark_bar()
|
|
|
|
| 201 |
.transform_filter(click_dist)
|
| 202 |
)
|
| 203 |
|
| 204 |
+
# 🌟 Line chart - 坚持使用全量数据集 df
|
| 205 |
period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)",
|
| 206 |
"Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"]
|
| 207 |
period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"]
|
|
|
|
| 253 |
).resolve_scale(color="shared")
|
| 254 |
|
| 255 |
dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent")
|
| 256 |
+
|
| 257 |
+
# 修复���版警告,使用 width="stretch" (如果报错可改回 use_container_width=True)
|
| 258 |
st.altair_chart(dashboard, use_container_width=True)
|
| 259 |
|
| 260 |
# ---------------------------------------------------------------------------
|
| 261 |
# SECTION 2 — When do crimes happen? heatmap + dropdown
|
|
|
|
|
|
|
| 262 |
# ---------------------------------------------------------------------------
|
| 263 |
st.markdown("---")
|
| 264 |
st.header("When Do Crimes Happen in Chicago?")
|
|
|
|
| 268 |
the heatmap by crime category, or leave it on *All* to see the overall pattern.
|
| 269 |
Each cell shows the total number of incidents at that day-of-week x hour-of-day
|
| 270 |
combination across the full dataset; darker red means more incidents.
|
|
|
|
|
|
|
|
|
|
| 271 |
"""
|
| 272 |
)
|
| 273 |
|
| 274 |
top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist()
|
| 275 |
selected_hm = st.selectbox("Select Crime Type", options=["All"] + top_types_hm, index=0)
|
| 276 |
|
|
|
|
| 277 |
weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
| 278 |
hm_source = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm]
|
| 279 |
hm_agg = (
|
|
|
|
| 298 |
)
|
| 299 |
.properties(
|
| 300 |
width=700, height=380,
|
| 301 |
+
title=f"Crime Heatmap - {selected_hm} (full dataset)",
|
| 302 |
)
|
| 303 |
)
|
| 304 |
st.altair_chart(heatmap, use_container_width=True)
|
| 305 |
|
| 306 |
# ---------------------------------------------------------------------------
|
| 307 |
# SECTION 3 — Poverty vs. Crime
|
|
|
|
|
|
|
| 308 |
# ---------------------------------------------------------------------------
|
| 309 |
st.markdown("---")
|
| 310 |
st.header("Does Poverty Predict Crime?")
|
|
|
|
| 312 |
"""
|
| 313 |
Socioeconomic inequality is one of the most studied predictors of crime at the
|
| 314 |
neighborhood level. The choropleth map on the left shades each of Chicago's 77
|
| 315 |
+
community areas by their poverty rate.
|
| 316 |
+
The scatter plot on the right makes the poverty-crime relationship explicit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
"""
|
| 318 |
)
|
| 319 |
|
|
|
|
| 340 |
.properties(width=360, height=440, title="Chicago Poverty Rate by Community Area")
|
| 341 |
)
|
| 342 |
|
| 343 |
+
# 🌟 核心修复:预先聚合经纬度,解决 longitude/latitude 不能应用 bin 的严重报错问题!
|
| 344 |
+
df_geo_binned = df_geo.copy()
|
| 345 |
+
df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(2)
|
| 346 |
+
df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(2)
|
| 347 |
+
density_agg = df_geo_binned.groupby(['lon_bin', 'lat_bin']).size().reset_index(name='incident_count')
|
| 348 |
+
|
| 349 |
crime_density = (
|
| 350 |
+
alt.Chart(density_agg)
|
| 351 |
+
.mark_square(opacity=0.7)
|
| 352 |
.encode(
|
| 353 |
+
longitude="lon_bin:Q",
|
| 354 |
+
latitude="lat_bin:Q",
|
| 355 |
color=alt.Color(
|
| 356 |
+
"incident_count:Q",
|
| 357 |
scale=alt.Scale(scheme="blues"),
|
| 358 |
title="Incident Count",
|
| 359 |
legend=alt.Legend(title="Incidents"),
|
| 360 |
),
|
| 361 |
+
size=alt.Size(
|
| 362 |
+
"incident_count:Q",
|
| 363 |
+
scale=alt.Scale(range=[10, 150]),
|
| 364 |
+
legend=None
|
| 365 |
+
),
|
| 366 |
+
tooltip=[
|
| 367 |
+
alt.Tooltip("lon_bin:Q", title="Longitude (Grid)"),
|
| 368 |
+
alt.Tooltip("lat_bin:Q", title="Latitude (Grid)"),
|
| 369 |
+
alt.Tooltip("incident_count:Q", title="Incidents")
|
| 370 |
+
]
|
| 371 |
)
|
| 372 |
)
|
| 373 |
|
|
|
|
| 380 |
|
| 381 |
with col4:
|
| 382 |
if not df_socio.empty and df["community_area"].notna().any():
|
|
|
|
| 383 |
df_crime_count = (
|
| 384 |
df.dropna(subset=["community_area"])
|
| 385 |
.groupby("community_area").size()
|
|
|
|
| 415 |
st.altair_chart(
|
| 416 |
(sc + reg).properties(
|
| 417 |
width=360, height=440,
|
| 418 |
+
title="Higher Poverty -> More Crimes?",
|
| 419 |
),
|
| 420 |
use_container_width=True,
|
| 421 |
)
|
|
|
|
| 437 |
| 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) |
|
| 438 |
| Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) |
|
| 439 |
| Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) |
|
| 440 |
+
|
| 441 |
All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/).
|
| 442 |
"""
|
| 443 |
)
|