Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +98 -100
src/streamlit_app.py
CHANGED
|
@@ -1,146 +1,144 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
| 3 |
import altair as alt
|
|
|
|
| 4 |
import json
|
| 5 |
|
| 6 |
-
# ================= 1. 页面
|
| 7 |
-
st.set_page_config(
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
)
|
| 11 |
|
| 12 |
-
# ================= 2. 数据加载与
|
| 13 |
@st.cache_data
|
| 14 |
-
def
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 18 |
try:
|
| 19 |
-
df = pd.read_csv(
|
| 20 |
except:
|
| 21 |
-
# 如果本地没找到,可以尝试从你的 GitHub raw 链接读取作为备用
|
| 22 |
url = "https://raw.githubusercontent.com/xinyic11/IS445_Final/main/Crimes_-_2026_20260417.csv"
|
| 23 |
df = pd.read_csv(url, low_memory=False)
|
| 24 |
-
|
| 25 |
-
# 基础清理
|
| 26 |
-
df['Date'] = pd.to_datetime(df['Date'], format='%m/%d/%Y %I:%M:%S %p', errors='coerce')
|
| 27 |
-
clean_df = df.dropna(subset=['Longitude', 'Latitude']).copy()
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
elif 12 < hour <= 18: return 'Afternoon (12pm-6pm)'
|
| 38 |
-
elif 18 < hour <= 24: return 'Evening (6pm-12am)'
|
| 39 |
-
else: return 'Late Night (12am-6am)'
|
| 40 |
-
clean_df['Period'] = clean_df['Hour'].apply(get_period)
|
| 41 |
|
| 42 |
-
|
| 43 |
-
cols_to_keep = ['Longitude', 'Latitude', 'District', 'District_Str', 'Primary Type', 'Date_Only', 'Period', 'Hour']
|
| 44 |
-
return clean_df[cols_to_keep]
|
| 45 |
-
|
| 46 |
-
# 加载数据
|
| 47 |
-
data = load_data()
|
| 48 |
-
|
| 49 |
-
# ================= 3. 页面标题与导言 =================
|
| 50 |
-
st.title("Rhythms of the City: A 2026 Chicago Crime Perspective")
|
| 51 |
-
st.markdown("### Authors: Group 6 (Xinyi Chen, Zhongyin Wang)")
|
| 52 |
|
| 53 |
-
|
| 54 |
-
Crime is rarely a random occurrence; it is a complex tapestry woven from geography, time, and social conditions.
|
| 55 |
-
This report dives into tens of thousands of crime records from the 2026 Chicago dataset to uncover where, when, and why these incidents happen.
|
| 56 |
-
""")
|
| 57 |
|
| 58 |
-
# =================
|
| 59 |
st.header("1. Central Exploration: The Crime Landscape")
|
| 60 |
-
st.write("点击地图上的警区
|
| 61 |
|
| 62 |
-
#
|
| 63 |
click_dist = alt.selection_point(fields=['District_Str'])
|
| 64 |
click_type = alt.selection_point(fields=['Primary Type'])
|
| 65 |
|
| 66 |
-
#
|
| 67 |
-
df_dist_counts = data.groupby('District_Str').size().reset_index(name='crime_count')
|
| 68 |
geojson_url = 'https://data.cityofchicago.org/resource/24zt-jpfn.geojson'
|
| 69 |
districts_geo = alt.Data(url=geojson_url, format=alt.DataFormat(property='features', type='json'))
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
stroke='white', strokeWidth=1
|
|
|
|
|
|
|
|
|
|
| 73 |
).transform_lookup(
|
| 74 |
-
lookup='
|
| 75 |
from_=alt.LookupData(df_dist_counts, 'District_Str', ['crime_count'])
|
| 76 |
).encode(
|
| 77 |
color=alt.condition(click_dist,
|
| 78 |
-
alt.Color('crime_count:Q', scale=alt.Scale(scheme='reds'), title='
|
| 79 |
alt.value('lightgrey')),
|
| 80 |
-
tooltip=[alt.Tooltip('properties.dist_num:N', title='
|
| 81 |
-
|
| 82 |
-
).add_params(click_dist).properties(width=380, height=400)
|
| 83 |
|
| 84 |
-
#
|
| 85 |
-
type_chart = alt.Chart(
|
| 86 |
-
x=alt.X('count():Q', title='
|
| 87 |
y=alt.Y('Primary Type:N', sort='-x', title=None),
|
| 88 |
color=alt.condition(click_type, alt.value('steelblue'), alt.value('lightgray')),
|
| 89 |
tooltip=['Primary Type', 'count()']
|
| 90 |
-
).add_params(click_type).transform_filter(click_dist).properties(width=
|
| 91 |
-
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
color=alt.datum('Total Daily'),
|
| 109 |
-
tooltip=[alt.Tooltip('Date_Only:T', title='Date'), alt.Tooltip('count():Q', title='Total')]
|
| 110 |
-
).transform_filter(click_dist).transform_filter(click_type)
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
st.header("2. Temporal Patterns: The City's Heartbeat")
|
| 120 |
-
st.markdown("分析每周各时段的犯罪频率,揭示犯罪的高发窗口。")
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
df_heatmap = data[data['Primary Type'].isin(top_10)].copy()
|
| 125 |
|
| 126 |
-
#
|
| 127 |
-
|
|
|
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
|
|
|
| 138 |
|
|
|
|
| 139 |
|
| 140 |
-
# ================= 6. 数据来源
|
| 141 |
st.markdown("---")
|
| 142 |
-
st.markdown(""
|
| 143 |
-
**Data Sources:**
|
| 144 |
-
- Crimes - 2026 (Preliminary): [City of Chicago Data Portal](https://data.cityofchicago.org/)
|
| 145 |
-
- Boundaries - Police Districts: [City of Chicago Data Portal](https://data.cityofchicago.org/resource/24zt-jpfn.geojson)
|
| 146 |
-
""")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
| 3 |
import altair as alt
|
| 4 |
+
import urllib.request
|
| 5 |
import json
|
| 6 |
|
| 7 |
+
# ================= 1. 页面配置 =================
|
| 8 |
+
st.set_page_config(page_title="Chicago Crime 2026 Analysis", layout="wide")
|
| 9 |
+
|
| 10 |
+
st.title("Rhythms of the City: A 2026 Chicago Crime Perspective")
|
| 11 |
+
st.markdown("**Authors: Group 6 (Xinyi Chen, Zhongyin Wang)**")
|
| 12 |
|
| 13 |
+
# ================= 2. 数据加载与预处理 =================
|
| 14 |
@st.cache_data
|
| 15 |
+
def load_all_data():
|
| 16 |
+
# A. 犯罪数据
|
| 17 |
+
crime_file = "Crimes_-_2026_20260417.csv"
|
|
|
|
| 18 |
try:
|
| 19 |
+
df = pd.read_csv(crime_file, low_memory=False)
|
| 20 |
except:
|
|
|
|
| 21 |
url = "https://raw.githubusercontent.com/xinyic11/IS445_Final/main/Crimes_-_2026_20260417.csv"
|
| 22 |
df = pd.read_csv(url, low_memory=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
df['Date'] = pd.to_datetime(df['Date'], format='%m/%d/%Y %I:%M:%S %p', errors='coerce')
|
| 25 |
+
df = df.dropna(subset=['Longitude', 'Latitude']).copy()
|
| 26 |
+
df['Date_Only'] = df['Date'].dt.floor('D')
|
| 27 |
+
df['District_Str'] = pd.to_numeric(df['District'], errors='coerce').fillna(-1).astype(int).astype(str)
|
| 28 |
|
| 29 |
+
# B. 经济社会数据 (用于散点图)
|
| 30 |
+
socio_url = "https://data.cityofchicago.org/resource/kn9c-c2s2.json"
|
| 31 |
+
df_socio = pd.read_json(socio_url)
|
| 32 |
+
df_socio['ca'] = df_socio['ca'].astype(float).astype(int).astype(str)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
return df, df_socio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
df_crime, df_socio = load_all_data()
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# ================= 3. 中央交互 Dashboard (修复点击变白问题) =================
|
| 39 |
st.header("1. Central Exploration: The Crime Landscape")
|
| 40 |
+
st.write("点击地图上的警区,或点击右侧条形图中的犯罪类型,下方时间轴会随之联动。")
|
| 41 |
|
| 42 |
+
# 准备交互选择器
|
| 43 |
click_dist = alt.selection_point(fields=['District_Str'])
|
| 44 |
click_type = alt.selection_point(fields=['Primary Type'])
|
| 45 |
|
| 46 |
+
# A. 填色地图
|
|
|
|
| 47 |
geojson_url = 'https://data.cityofchicago.org/resource/24zt-jpfn.geojson'
|
| 48 |
districts_geo = alt.Data(url=geojson_url, format=alt.DataFormat(property='features', type='json'))
|
| 49 |
+
df_dist_counts = df_crime.groupby('District_Str').size().reset_index(name='crime_count')
|
| 50 |
|
| 51 |
+
map_chart = alt.Chart(districts_geo).mark_geoshape(
|
| 52 |
stroke='white', strokeWidth=1
|
| 53 |
+
).transform_calculate(
|
| 54 |
+
# 关键修复:确保地理数据的字段名与犯罪数据的 District_Str 匹配
|
| 55 |
+
District_Str = "datum.properties.dist_num"
|
| 56 |
).transform_lookup(
|
| 57 |
+
lookup='District_Str',
|
| 58 |
from_=alt.LookupData(df_dist_counts, 'District_Str', ['crime_count'])
|
| 59 |
).encode(
|
| 60 |
color=alt.condition(click_dist,
|
| 61 |
+
alt.Color('crime_count:Q', scale=alt.Scale(scheme='reds'), title='案件数'),
|
| 62 |
alt.value('lightgrey')),
|
| 63 |
+
tooltip=[alt.Tooltip('properties.dist_num:N', title='警区'), alt.Tooltip('crime_count:Q', title='总案件数')]
|
| 64 |
+
).add_params(click_dist).properties(width=400, height=450)
|
|
|
|
| 65 |
|
| 66 |
+
# B. 柱状图
|
| 67 |
+
type_chart = alt.Chart(df_crime).mark_bar().encode(
|
| 68 |
+
x=alt.X('count():Q', title='案件数量'),
|
| 69 |
y=alt.Y('Primary Type:N', sort='-x', title=None),
|
| 70 |
color=alt.condition(click_type, alt.value('steelblue'), alt.value('lightgray')),
|
| 71 |
tooltip=['Primary Type', 'count()']
|
| 72 |
+
).add_params(click_type).transform_filter(click_dist).properties(width=300, height=450)
|
| 73 |
+
|
| 74 |
+
# C. 时间线图
|
| 75 |
+
df_crime['Hour'] = df_crime['Date'].dt.hour
|
| 76 |
+
def get_period(h):
|
| 77 |
+
if 6 < h <= 12: return 'Morning (6am-12pm)'
|
| 78 |
+
elif 12 < h <= 18: return 'Afternoon (12pm-6pm)'
|
| 79 |
+
elif 18 < h <= 24: return 'Evening (6pm-12am)'
|
| 80 |
+
else: return 'Late Night (12am-6am)'
|
| 81 |
+
df_crime['Period'] = df_crime['Hour'].apply(get_period)
|
| 82 |
+
|
| 83 |
+
line_chart = alt.Chart(df_crime).mark_line().encode(
|
| 84 |
+
x=alt.X('Date_Only:T', title='时间轴'),
|
| 85 |
+
y=alt.Y('count():Q', title='案件数'),
|
| 86 |
+
color=alt.Color('Period:N', scale=alt.Scale(scheme='category10'), title='时段'),
|
| 87 |
+
tooltip=['Date_Only:T', 'count():Q']
|
| 88 |
+
).transform_filter(click_dist).transform_filter(click_type).properties(width=800, height=250)
|
| 89 |
+
|
| 90 |
+
st.altair_chart((map_chart | type_chart) & line_chart, theme=None)
|
| 91 |
+
|
| 92 |
+
# ================= 4. Heatmap (修复下拉框缺失问题) =================
|
| 93 |
+
st.header("2. Temporal Patterns: The City's Heartbeat")
|
| 94 |
|
| 95 |
+
# 在 Streamlit 中,使用 st.selectbox 代替 Vega 内部 binding 效果更好
|
| 96 |
+
top_types = df_crime['Primary Type'].value_counts().head(10).index.tolist()
|
| 97 |
+
selected_crime = st.selectbox("选择犯罪类型进行分析:", ["All"] + top_types)
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
# 过滤数据
|
| 100 |
+
df_heatmap = df_crime.copy()
|
| 101 |
+
if selected_crime != "All":
|
| 102 |
+
df_heatmap = df_heatmap[df_heatmap['Primary Type'] == selected_crime]
|
| 103 |
|
| 104 |
+
df_heatmap['Weekday'] = df_heatmap['Date'].dt.day_name().str[:3]
|
| 105 |
+
df_heatmap['Hour'] = df_heatmap['Date'].dt.hour
|
| 106 |
+
weekday_order = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
| 107 |
|
| 108 |
+
heatmap = alt.Chart(df_heatmap).mark_rect().encode(
|
| 109 |
+
x=alt.X('Weekday:N', sort=weekday_order, title='星期'),
|
| 110 |
+
y=alt.Y('Hour:O', title='小时'),
|
| 111 |
+
color=alt.Color('count():Q', scale=alt.Scale(scheme='reds'), title='案件数'),
|
| 112 |
+
tooltip=['Weekday', 'Hour', 'count()']
|
| 113 |
+
).properties(width=700, height=400, title=f"犯罪时间分布: {selected_crime}")
|
| 114 |
|
| 115 |
+
st.altair_chart(heatmap, use_container_width=True)
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
# ================= 5. 经济贫困散点图 (还原缺失的分析) =================
|
| 118 |
+
st.header("3. Socioeconomic Roots: Poverty and Safety")
|
|
|
|
| 119 |
|
| 120 |
+
# 数据聚合与合并逻辑
|
| 121 |
+
df_crime_count = df_crime.groupby('Community Area').size().reset_index(name='crime_count')
|
| 122 |
+
df_crime_count['ca'] = df_crime_count['Community Area'].astype(float).astype(int).astype(str)
|
| 123 |
|
| 124 |
+
df_scatter = pd.merge(
|
| 125 |
+
df_socio[['ca', 'community_area_name', 'poverty_rate']],
|
| 126 |
+
df_crime_count, on='ca', how='inner'
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
scatter = alt.Chart(df_scatter).mark_circle(size=80, opacity=0.75).encode(
|
| 130 |
+
x=alt.X('poverty_rate:Q', title='Poverty Rate (%)'),
|
| 131 |
+
y=alt.Y('crime_count:Q', title='案件总数 (2026)'),
|
| 132 |
+
color=alt.Color('poverty_rate:Q', scale=alt.Scale(scheme='orangered'), legend=None),
|
| 133 |
+
tooltip=['community_area_name:N', 'poverty_rate:Q', 'crime_count:Q']
|
| 134 |
+
)
|
| 135 |
|
| 136 |
+
regression = scatter.transform_regression(
|
| 137 |
+
'poverty_rate', 'crime_count'
|
| 138 |
+
).mark_line(color='gray', strokeDash=[4, 4])
|
| 139 |
|
| 140 |
+
st.altair_chart((scatter + regression).properties(width=800, height=400), use_container_width=True)
|
| 141 |
|
| 142 |
+
# ================= 6. 数据来源 =================
|
| 143 |
st.markdown("---")
|
| 144 |
+
st.markdown("数据来源于 [Chicago Data Portal](https://data.cityofchicago.org/)。")
|
|
|
|
|
|
|
|
|
|
|
|