Download src/streamlit_app.py from group6/final: direct link, hf CLI and curl.
- Browser
- Download file 18.9 kB
-
https://huggingface.co/spaces/group6/final/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/group6/final/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/group6/final/resolve/main/src/streamlit_app.py
18.9 kB
| import streamlit as st | |
| import pandas as pd | |
| import altair as alt | |
| import json | |
| import urllib.request | |
| st.set_page_config(page_title="Crimes in Chicago 2026", page_icon="🚨", layout="wide") | |
| st.title("Crimes in Chicago - 2026") | |
| st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** - Group 6") | |
| st.markdown("---") | |
| st.markdown( | |
| """ | |
| ## What Is This About? | |
| Every day, hundreds of crime incidents are reported across Chicago's 77 community areas. | |
| But where do they happen? At what time? And does poverty play a role? | |
| This interactive article walks you through 2026 Chicago crime data drawn directly from | |
| the [Chicago Data Portal](https://data.cityofchicago.org/) to help you explore the | |
| geography, timing, and social context of crime in one of America's largest cities. | |
| The dataset records every reported crime incident in 2026, including the exact location, | |
| date and time, crime type, and the police district that handled it. Each row is one | |
| reported incident. We also include community-level socioeconomic data to examine the | |
| relationship between poverty and crime rates across Chicago's neighborhoods. | |
| """ | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Data loading (Now using local CSV for extreme speedup) | |
| # --------------------------------------------------------------------------- | |
| def load_crime_data(): | |
| """Robust loading for Hugging Face Spaces (handles path issues).""" | |
| import os | |
| try: | |
| # 获取当前脚本所在目录 | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| file_path = os.path.join(BASE_DIR, "Crimes_-_2026_20260417.csv") | |
| # 读取 CSV | |
| df = pd.read_csv(file_path) | |
| # 标准化列名 | |
| df.columns = [c.lower().replace(" ", "_") for c in df.columns] | |
| except FileNotFoundError: | |
| st.error("❌ CSV file not found. Check filename and path.") | |
| return pd.DataFrame() | |
| except Exception as e: | |
| st.error(f"❌ Failed to read CSV: {e}") | |
| return pd.DataFrame() | |
| # 后处理 | |
| df["date"] = pd.to_datetime(df["date"], errors="coerce") | |
| for col in ["latitude", "longitude"]: | |
| df[col] = pd.to_numeric(df.get(col, pd.Series(dtype=float)), errors="coerce") | |
| df = df.dropna(subset=["date"]) | |
| df["Date_Only"] = df["date"].dt.floor("d") | |
| df["Hour"] = df["date"].dt.hour | |
| df["weekday"] = df["date"].dt.day_name().str[:3] | |
| df["Primary Type"] = ( | |
| df["primary_type"].str.upper() | |
| if "primary_type" in df.columns else "UNKNOWN" | |
| ) | |
| if "district" in df.columns: | |
| df["District_Str"] = ( | |
| pd.to_numeric(df["district"], errors="coerce") | |
| .fillna(-1).astype(int).astype(str) | |
| ) | |
| df["District"] = df["District_Str"] | |
| else: | |
| df["District_Str"] = df["District"] = "-1" | |
| if "community_area" not in df.columns: | |
| df["community_area"] = None | |
| def get_period(h): | |
| if 6 < h <= 12: | |
| return "Morning (6am-12pm)" | |
| elif 12 < h <= 18: | |
| return "Afternoon (12pm-6pm)" | |
| elif 18 < h <= 24: | |
| return "Evening (6pm-12am)" | |
| else: | |
| return "Late Night (12am-6am)" | |
| df["Period"] = df["Hour"].apply(get_period) | |
| return df | |
| def load_socio(): | |
| try: | |
| df = pd.read_json("https://data.cityofchicago.org/resource/kn9c-c2s2.json") | |
| df = df.dropna(subset=["ca"]) | |
| df["ca"] = df["ca"].astype(float).astype(int).astype(str) | |
| df["poverty_rate"] = pd.to_numeric(df["percent_households_below_poverty"], errors="coerce") | |
| return df | |
| except Exception as e: | |
| st.warning(f"Could not load socioeconomic data: {e}") | |
| return pd.DataFrame(columns=["ca", "community_area_name", "poverty_rate"]) | |
| def load_geojson(url): | |
| try: | |
| with urllib.request.urlopen(url) as r: | |
| return json.loads(r.read()) | |
| except Exception as e: | |
| st.warning(f"Could not load GeoJSON: {e}") | |
| return {"features": []} | |
| district_geojson = load_geojson("https://data.cityofchicago.org/resource/24zt-jpfn.geojson") | |
| community_geojson = load_geojson("https://data.cityofchicago.org/resource/igwz-8jzy.geojson") | |
| df = load_crime_data() | |
| df_socio = load_socio() | |
| districts = alt.Data(values=district_geojson["features"]) | |
| communities = alt.Data(values=community_geojson["features"]) | |
| if df.empty: | |
| st.error("Crime data could not be loaded.") | |
| st.stop() | |
| df_geo = df.dropna(subset=["latitude", "longitude"]).copy() | |
| st.info(f"Loaded **{len(df):,}** crime records for 2026 ({len(df_geo):,} with coordinates).") | |
| # --------------------------------------------------------------------------- | |
| # SECTION 1 — Linked dashboard | |
| # --------------------------------------------------------------------------- | |
| st.markdown("---") | |
| st.header("Interactive Crime Dashboard") | |
| st.markdown( | |
| """ | |
| This dashboard lets you explore Chicago crime data across three linked views. | |
| **Drag a box on the map** to select a geographic area, or **click a district boundary** | |
| to highlight it — both actions filter the bar chart on the right and the timeline below. | |
| You can also **click a crime category** in the bar chart to drill into its temporal trend. | |
| *(Note: If the map points look like a strict grid, it is because the Chicago Police Department | |
| anonymizes crime locations to the nearest block level, aligning perfectly with Chicago's grid street system!)* | |
| """ | |
| ) | |
| brush = alt.selection_interval(name="brush") | |
| click_type = alt.selection_point(fields=["Primary Type"], name="click_type") | |
| click_dist = alt.selection_point(fields=["District_Str"], name="click_dist") | |
| MAP_SAMPLE = 5000 | |
| df_map_sample = df_geo.sample(min(MAP_SAMPLE, len(df_geo)), random_state=42) | |
| background = ( | |
| alt.Chart(districts) | |
| .mark_geoshape(stroke="black", strokeWidth=0.6) | |
| .transform_calculate(District_Str="datum.properties.dist_num") | |
| .encode( | |
| color=alt.condition(click_dist, alt.value("white"), alt.value("grey")), | |
| opacity=alt.condition(click_dist, alt.value(0.5), alt.value(0.8)), | |
| tooltip=[alt.Tooltip("properties.dist_num:N", title="District")], | |
| ) | |
| .add_params(click_dist) | |
| ) | |
| geo_points = ( | |
| alt.Chart(df_map_sample) | |
| .mark_circle(size=5) | |
| .encode( | |
| longitude="longitude:Q", | |
| latitude="latitude:Q", | |
| color=alt.condition( | |
| click_dist, | |
| alt.Color("District:N", scale=alt.Scale(scheme="tableau10"), | |
| legend=alt.Legend(title="District", orient="right")), | |
| alt.value("#e0dbd6"), | |
| ), | |
| opacity=alt.condition(click_dist, alt.value(0.6), alt.value(0.05)), | |
| tooltip=[ | |
| alt.Tooltip("Primary Type:N", title="Crime Type"), | |
| alt.Tooltip("District:N", title="District"), | |
| alt.Tooltip("date:T", title="Date"), | |
| ], | |
| ) | |
| .add_params(brush) | |
| ) | |
| map_layer = (background + geo_points).project(type="mercator").properties( | |
| width=420, height=450, | |
| title=f"Chicago Crime Map (map shows {MAP_SAMPLE:,} sampled points for performance)", | |
| ) | |
| # Bar chart - full df | |
| type_chart = ( | |
| alt.Chart(df) | |
| .mark_bar() | |
| .encode( | |
| x=alt.X("count():Q", title="Number of Crimes"), | |
| y=alt.Y("Primary Type:N", sort="-x", title="Crime Type"), | |
| color=alt.condition(click_type, alt.value("steelblue"), alt.value("lightgray")), | |
| tooltip=["Primary Type:N", "count():Q"], | |
| ) | |
| .properties(width=300, height=450, title="Crime Types (full dataset)") | |
| .add_params(click_type) | |
| .transform_filter(brush) | |
| .transform_filter(click_dist) | |
| ) | |
| # Line chart - full df | |
| period_order = ["Morning (6am-12pm)", "Afternoon (12pm-6pm)", | |
| "Evening (6pm-12am)", "Late Night (12am-6am)", "Total Daily"] | |
| period_range = ["#f4a261", "#e9c46a", "#e76f51", "#264653", "grey"] | |
| period_lines = ( | |
| alt.Chart(df) | |
| .mark_line(point=False, strokeWidth=1.5) | |
| .encode( | |
| x=alt.X("Date_Only:T", title="Timeline"), | |
| y=alt.Y("count:Q", title="Number of Incidents", scale=alt.Scale(zero=True)), | |
| color=alt.Color( | |
| "Period:N", | |
| scale=alt.Scale(domain=period_order, range=period_range), | |
| legend=alt.Legend(title="Time of Day", orient="right"), | |
| ), | |
| tooltip=[ | |
| alt.Tooltip("Date_Only:T", title="Date"), | |
| alt.Tooltip("Period:N", title="Period"), | |
| alt.Tooltip("count:Q", title="Incidents"), | |
| ], | |
| ) | |
| .transform_filter(brush) | |
| .transform_filter(click_type) | |
| .transform_filter(click_dist) | |
| .transform_aggregate(count="count()", groupby=["Date_Only", "Period"]) | |
| .transform_impute(impute="count", key="Date_Only", groupby=["Period"], value=0) | |
| ) | |
| total_line = ( | |
| alt.Chart(df) | |
| .mark_line(opacity=0.5) | |
| .encode( | |
| x=alt.X("Date_Only:T"), | |
| y=alt.Y("count():Q"), | |
| color=alt.datum("Total Daily"), | |
| tooltip=[ | |
| alt.Tooltip("Date_Only:T", title="Date"), | |
| alt.Tooltip("count():Q", title="Total Incidents"), | |
| ], | |
| ) | |
| .transform_filter(brush) | |
| .transform_filter(click_type) | |
| .transform_filter(click_dist) | |
| ) | |
| line_chart = (total_line + period_lines).properties( | |
| width=760, height=220, | |
| title="Daily Crime Trend by Time of Day (full dataset)", | |
| ).resolve_scale(color="shared") | |
| dashboard = ((map_layer | type_chart) & line_chart).resolve_scale(color="independent") | |
| st.altair_chart(dashboard, use_container_width=True) | |
| # --------------------------------------------------------------------------- | |
| # SECTION 2 — When do crimes happen? heatmap + dropdown (NOW LAG-FREE) | |
| # --------------------------------------------------------------------------- | |
| st.markdown("---") | |
| st.header("When Do Crimes Happen in Chicago?") | |
| st.markdown( | |
| """ | |
| Different crimes follow different schedules. Use the **dropdown below** to filter | |
| the heatmap by crime category. | |
| *(This is now fully interactive in your browser, filtering happens instantly without lag!)* | |
| """ | |
| ) | |
| # 提取前10大罪案类型 | |
| top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist() | |
| # 在 Python 预先计算所有 (Primary Type, weekday, Hour) 的聚合数量,减少传到前端的数据量 | |
| hm_agg = ( | |
| df.dropna(subset=["Primary Type"]) | |
| .groupby(["Primary Type", "weekday", "Hour"]) | |
| .size() | |
| .reset_index(name="crime_count") | |
| ) | |
| # 🔥 核心提速秘籍:创建一个 Altair 原生的下拉绑定,把过滤操作全推给前端浏览器做,不重启 Streamlit! | |
| dropdown = alt.binding_select( | |
| options=[None] + top_types_hm, | |
| labels=["All"] + top_types_hm, | |
| name="Filter by Crime Type: " | |
| ) | |
| type_select = alt.selection_point(fields=["Primary Type"], bind=dropdown) | |
| weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] | |
| heatmap = ( | |
| alt.Chart(hm_agg) | |
| .mark_rect() | |
| .encode( | |
| x=alt.X("weekday:N", sort=weekday_order, title="Day of Week"), | |
| y=alt.Y("Hour:O", title="Hour of Day (0-23)", sort="ascending"), | |
| # 使用 sum(crime_count) 确保选 All 的时候数字正确累加 | |
| color=alt.Color("sum(crime_count):Q", scale=alt.Scale(scheme="reds"), title="Number of Crimes"), | |
| tooltip=[ | |
| alt.Tooltip("weekday:N", title="Day"), | |
| alt.Tooltip("Hour:O", title="Hour"), | |
| alt.Tooltip("sum(crime_count):Q", title="Total Crimes"), | |
| ], | |
| ) | |
| .add_params(type_select) # 绑定前端选择器 | |
| .transform_filter(type_select) # 让图表根据选择器过滤数据 | |
| .properties( | |
| width=700, height=380, | |
| title="Crime Heatmap (Instantly filterable)", | |
| ) | |
| ) | |
| st.altair_chart(heatmap, use_container_width=True) | |
| # --------------------------------------------------------------------------- | |
| # SECTION 3 — Poverty vs. Crime | |
| # --------------------------------------------------------------------------- | |
| st.markdown("---") | |
| st.header("Does Poverty Predict Crime?") | |
| st.markdown( | |
| """ | |
| Socioeconomic inequality is one of the most studied predictors of crime at the | |
| neighborhood level. The choropleth map on the left shades each of Chicago's 77 | |
| community areas by their poverty rate - darker orange means higher poverty - | |
| with a binned crime density heatmap overlaid. The heatmap uses the full dataset | |
| with no sampling: each cell's color reflects how many incidents fall in that | |
| geographic bin, giving a clear picture of crime hotspots. | |
| The scatter plot on the right makes the poverty-crime relationship explicit: | |
| each dot is one community area, and the dashed line is a statistical trend. | |
| There is a moderate positive correlation, though it is far from deterministic - | |
| policy, policing patterns, and reporting rates all play a role. | |
| **Socioeconomic data source:** [Census Data - Chicago Data Portal](https://data.cityofchicago.org/Health-Human-Services/Census-Data-Selected-Socioeconomic-Indicators-in-C/kn9c-c2s2) | |
| """ | |
| ) | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| if not df_socio.empty and community_geojson["features"]: | |
| poverty_map = ( | |
| alt.Chart(communities) | |
| .mark_geoshape(stroke="white", strokeWidth=0.4) | |
| .transform_lookup( | |
| lookup="properties.area_num_1", | |
| from_=alt.LookupData(df_socio, "ca", ["poverty_rate", "community_area_name"]), | |
| ) | |
| .encode( | |
| color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"), | |
| title="Poverty Rate (%)"), | |
| tooltip=[ | |
| alt.Tooltip("properties.community:N", title="Community"), | |
| alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"), | |
| ], | |
| ) | |
| .project(type="mercator") | |
| .properties(width=360, height=440, title="Chicago Poverty Rate by Community Area") | |
| ) | |
| # --- FIX: 改成了更高精度的 round(3) 结合 mark_circle 来实现细腻的热力图外观 --- | |
| df_geo_binned = df_geo.copy() | |
| # round(3) 大约对应100米的网格,比原来的 1.1公里 (round 2) 精细很多 | |
| df_geo_binned['lat_bin'] = df_geo_binned['latitude'].round(3) | |
| df_geo_binned['lon_bin'] = df_geo_binned['longitude'].round(3) | |
| # 统计每个细微网格的案件数量 | |
| density_agg = df_geo_binned.groupby(['lat_bin', 'lon_bin']).size().reset_index(name='incident_count') | |
| # Binned geo-heatmap: 使用半透明的小圆点(mark_circle)模拟完美的热力云图 | |
| crime_density = ( | |
| alt.Chart(density_agg) | |
| .mark_circle(opacity=0.6, size=15) # 调小了size,换成了圆形 | |
| .encode( | |
| longitude="lon_bin:Q", | |
| latitude="lat_bin:Q", | |
| color=alt.Color( | |
| "incident_count:Q", | |
| scale=alt.Scale(scheme="blues"), | |
| title="Incident Count", | |
| legend=alt.Legend(title="Incidents"), | |
| ), | |
| tooltip=[ | |
| alt.Tooltip("incident_count:Q", title="Total Incidents") | |
| ] | |
| ) | |
| ) | |
| st.altair_chart( | |
| (poverty_map + crime_density).resolve_scale(color="independent"), | |
| use_container_width=True, | |
| ) | |
| else: | |
| st.info("Socioeconomic or boundary data unavailable.") | |
| with col4: | |
| if not df_socio.empty and df["community_area"].notna().any(): | |
| df_crime_count = ( | |
| df.dropna(subset=["community_area"]) | |
| .groupby("community_area").size() | |
| .reset_index(name="crime_count") | |
| ) | |
| df_crime_count["ca"] = ( | |
| df_crime_count["community_area"].astype(float).astype(int).astype(str) | |
| ) | |
| df_scatter = pd.merge( | |
| df_socio[["ca", "community_area_name", "poverty_rate"]], | |
| df_crime_count[["ca", "crime_count"]], | |
| on="ca", how="inner", | |
| ) | |
| if len(df_scatter) > 5: | |
| sc = ( | |
| alt.Chart(df_scatter) | |
| .mark_circle(size=80, opacity=0.75) | |
| .encode( | |
| x=alt.X("poverty_rate:Q", title="Poverty Rate (%)"), | |
| y=alt.Y("crime_count:Q", title="Crime Count (2026)"), | |
| color=alt.Color("poverty_rate:Q", scale=alt.Scale(scheme="orangered"), | |
| legend=None), | |
| tooltip=[ | |
| alt.Tooltip("community_area_name:N", title="Community"), | |
| alt.Tooltip("poverty_rate:Q", title="Poverty Rate (%)", format=".1f"), | |
| alt.Tooltip("crime_count:Q", title="Crime Count"), | |
| ], | |
| ) | |
| ) | |
| reg = sc.transform_regression("poverty_rate", "crime_count").mark_line( | |
| color="gray", strokeDash=[4, 4], strokeWidth=1.5 | |
| ) | |
| st.altair_chart( | |
| (sc + reg).properties( | |
| width=360, height=440, | |
| title="Higher Poverty -> More Crimes? (each dot = one community area)", | |
| ), | |
| use_container_width=True, | |
| ) | |
| else: | |
| st.info("Not enough community-level overlap to render scatter plot.") | |
| else: | |
| st.info("Community area data not available in this dataset sample.") | |
| # --------------------------------------------------------------------------- | |
| # Citations | |
| # --------------------------------------------------------------------------- | |
| st.markdown("---") | |
| st.header("Data Sources & Citations") | |
| st.markdown( | |
| """ | |
| | Dataset | Source | Link | | |
| |---|---|---| | |
| | Chicago Crimes 2001-Present | City of Chicago Data Portal | [ijzp-q8t2](https://data.cityofchicago.org/Public-Safety/Crimes-2001-to-Present/ijzp-q8t2) | | |
| | 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) | | |
| | Police District Boundaries (GeoJSON) | City of Chicago Data Portal | [24zt-jpfn](https://data.cityofchicago.org/Public-Safety/Boundaries-Police-Districts-current-/24zt-jpfn) | | |
| | Community Area Boundaries (GeoJSON) | City of Chicago Data Portal | [igwz-8jzy](https://data.cityofchicago.org/Facilities-Geographic-Boundaries/Boundaries-Community-Areas-current-/cauq-8yn6) | | |
| All data accessed April 2026. Visualizations built with [Altair](https://altair-viz.github.io/) and [Streamlit](https://streamlit.io/). | |
| """ | |
| ) |