Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import altair as alt | |
| import json | |
| import urllib.request | |
| # ββ Page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.set_page_config( | |
| page_title="Crimes in Chicago 2026", | |
| page_icon="π", | |
| layout="wide", | |
| ) | |
| # ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| st.title("π Crimes in Chicago β 2026") | |
| st.markdown("**Authors: Xinyi Chen, Zhongyin Wang** Β· Group 6") | |
| st.markdown("---") | |
| # ββ Introduction ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_crime_data(): | |
| url = ( | |
| "https://data.cityofchicago.org/resource/ijzp-q8t2.json" | |
| "?$where=year=2026" | |
| "&$limit=10000" | |
| "&$order=date%20DESC" | |
| ) | |
| try: | |
| df = pd.read_json(url) | |
| except Exception as e: | |
| st.error(f"Failed to load crime data: {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=["latitude", "longitude", "date"]) | |
| df["Date_Only"] = df["date"].dt.floor("d") | |
| df["Hour"] = df["date"].dt.hour | |
| df["weekday"] = df["date"].dt.day_name().str[:3] | |
| if "primary_type" in df.columns: | |
| df["Primary Type"] = df["primary_type"].str.upper() | |
| else: | |
| df["Primary Type"] = "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"] = "-1" | |
| df["District"] = "-1" | |
| if "community_area" not in df.columns: | |
| df["community_area"] = None | |
| # Period column | |
| def get_period(hour): | |
| if 6 < hour <= 12: | |
| return "Morning (6am-12pm)" | |
| elif 12 < hour <= 18: | |
| return "Afternoon (12pm-6pm)" | |
| elif 18 < hour <= 24: | |
| return "Evening (6pm-12am)" | |
| else: | |
| return "Late Night (12am-6am)" | |
| df["Period"] = df["Hour"].apply(get_period) | |
| return df | |
| def load_socio(): | |
| url = "https://data.cityofchicago.org/resource/kn9c-c2s2.json" | |
| try: | |
| df = pd.read_json(url) | |
| 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. Please check the Chicago Data Portal.") | |
| st.stop() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SECTION 1 β Linked dashboard (map | bar chart) & time-of-day line chart | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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. | |
| The bottom line chart breaks daily incident counts into four time-of-day periods | |
| (plus a total), so you can see not just *where* crime happens but *when* it peaks. | |
| """ | |
| ) | |
| # Altair selections (note: cross-chart filtering via selections only works when | |
| # the entire compound chart is rendered as one Altair object, which st.altair_chart supports) | |
| 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 layer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| .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="Chicago Crime Map (Brush to select area / Click district)", | |
| ) | |
| # ββ Crime-type bar chart ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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") | |
| .add_params(click_type) | |
| .transform_filter(brush) | |
| .transform_filter(click_dist) | |
| ) | |
| # ββ Time-of-day line chart ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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", | |
| ).resolve_scale(color="shared") | |
| # ββ Compose full dashboard ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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? (standalone heatmap + dropdown) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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 to a specific crime category β or leave it on *All* to see the | |
| overall pattern. Each cell shows the total number of incidents at that | |
| day-of-week Γ hour-of-day combination; darker red means more incidents. | |
| Across nearly every category, Friday and Saturday evenings (6 pm β midnight) | |
| stand out as the most active windows, while the early morning hours (2β5 am) | |
| are quietest β except for a few crime types that peak overnight. | |
| """ | |
| ) | |
| top_types_hm = df["Primary Type"].value_counts().head(10).index.tolist() | |
| selected_hm = st.selectbox( | |
| "Select Crime Type", | |
| options=["All"] + top_types_hm, | |
| index=0, | |
| ) | |
| hm_df = df if selected_hm == "All" else df[df["Primary Type"] == selected_hm] | |
| weekday_order = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] | |
| heatmap = ( | |
| alt.Chart(hm_df) | |
| .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"), | |
| color=alt.Color( | |
| "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("count():Q", title="Total Crimes"), | |
| ], | |
| ) | |
| .properties( | |
| width=700, height=380, | |
| title=alt.TitleParams( | |
| text=f"Crime Heatmap β {selected_hm}", | |
| subtitle="Select a crime type above to filter Β· Darker = more incidents", | |
| fontSize=14, | |
| ), | |
| ) | |
| ) | |
| 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 crime incident dots overlaid in blue. | |
| A visual comparison suggests that some of the highest-crime community areas, | |
| particularly on the South and West sides, also carry the heaviest poverty burden. | |
| The scatter plot on the right makes this 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") | |
| ) | |
| crime_overlay = ( | |
| alt.Chart(df.sample(min(5000, len(df)), random_state=42)) | |
| .mark_circle(size=3, color="steelblue", opacity=0.3) | |
| .encode(longitude="longitude:Q", latitude="latitude:Q") | |
| ) | |
| st.altair_chart(poverty_map + crime_overlay, 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=alt.TitleParams( | |
| text="Higher Poverty β More Crimes?", | |
| subtitle="Each dot = one community area | Dashed = trend", | |
| fontSize=13, | |
| ), | |
| ), | |
| 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/). | |
| """ | |
| ) |