XinyiC11 commited on
Commit
759bbf7
·
verified ·
1 Parent(s): 9d561aa

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. 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
- page_title="Chicago Crime Analysis 2026",
9
- layout="wide"
10
- )
11
 
12
- # ================= 2. 数据加载与(使用缓存) =================
13
  @st.cache_data
14
- def load_data():
15
- # 注意:请确保你的 CSV 文件名与此处一致
16
- file_path = "Crimes_-_2026_20260417.csv"
17
-
18
  try:
19
- df = pd.read_csv(file_path, low_memory=False)
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
- # 🌟 修复 1:使用大写 'D' 消除 Pandas 弃用警告
30
- clean_df['Date_Only'] = clean_df['Date'].dt.floor('D')
31
- clean_df['District_Str'] = pd.to_numeric(clean_df['District'], errors='coerce').fillna(-1).astype(int).astype(str)
 
32
 
33
- # 计算时段 Period
34
- clean_df['Hour'] = clean_df['Date'].dt.hour
35
- def get_period(hour):
36
- if 6 < hour <= 12: return 'Morning (6am-12pm)'
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
- st.markdown("""
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
- # ================= 4. 核心交互图表 (Dashboard) =================
59
  st.header("1. Central Exploration: The Crime Landscape")
60
- st.write("点击地图上的警区(District)或右侧的犯罪类型(Crime Type)来联动筛选下方时间轴。")
61
 
62
- # --- 准备 Dashboard 的交互参数 ---
63
  click_dist = alt.selection_point(fields=['District_Str'])
64
  click_type = alt.selection_point(fields=['Primary Type'])
65
 
66
- # --- A. 填色地图 (优化方案,秒开不卡顿) ---
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
- choropleth = alt.Chart(districts_geo).mark_geoshape(
72
  stroke='white', strokeWidth=1
 
 
 
73
  ).transform_lookup(
74
- lookup='properties.dist_num',
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='Crimes'),
79
  alt.value('lightgrey')),
80
- tooltip=[alt.Tooltip('properties.dist_num:N', title='District'),
81
- alt.Tooltip('crime_count:Q', title='Total Crimes')]
82
- ).add_params(click_dist).properties(width=380, height=400)
83
 
84
- # --- B. 犯罪类型柱状图 ---
85
- type_chart = alt.Chart(data).mark_bar().encode(
86
- x=alt.X('count():Q', title='Number of Crimes'),
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=250, height=400)
91
-
92
- # --- C. 时间趋势图 (含灰色总线) ---
93
- period_order = ['Morning (6am-12pm)', 'Afternoon (12pm-6pm)', 'Evening (6pm-12am)', 'Late Night (12am-6am)', 'Total Daily']
94
- period_range = ['#f4a261', '#e9c46a', '#e76f51', '#264653', '#888888']
95
-
96
- period_lines = alt.Chart(data).mark_line(strokeWidth=1.5).encode(
97
- x=alt.X('Date_Only:T', title='Timeline'),
98
- y=alt.Y('count:Q', title='Incidents'),
99
- color=alt.Color('Period:N', scale=alt.Scale(domain=period_order, range=period_range), legend=alt.Legend(title='Time of Day')),
100
- tooltip=['Date_Only:T', 'Period:N', 'count:Q']
101
- ).transform_filter(click_dist).transform_filter(click_type).transform_aggregate(
102
- count='count()', groupby=['Date_Only', 'Period']
103
- )
 
 
 
 
 
 
 
 
104
 
105
- total_line = alt.Chart(data).mark_line(opacity=0.3, strokeWidth=3).encode(
106
- x=alt.X('Date_Only:T'),
107
- y=alt.Y('count():Q'),
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
- line_chart = (total_line + period_lines).properties(width=700, height=250).resolve_scale(color='shared')
 
 
 
113
 
114
- # 🌟 修复 3a:使用 width='content' 消除 Streamlit 警告
115
- st.altair_chart((choropleth | type_chart) & line_chart, width='content')
 
116
 
 
 
 
 
 
 
117
 
118
- # ================= 5. 上下文图表 (Heatmap) =================
119
- st.header("2. Temporal Patterns: The City's Heartbeat")
120
- st.markdown("分析每周各时段的犯罪频率,揭示犯罪的高发窗口。")
121
 
122
- weekday_order = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
123
- top_10 = data['Primary Type'].value_counts().head(10).index.tolist()
124
- df_heatmap = data[data['Primary Type'].isin(top_10)].copy()
125
 
126
- # 🌟 修复 2:使用 Date_Only 提取星期几,消除 KeyError: 'Date' 报错
127
- df_heatmap['Weekday_Name'] = df_heatmap['Date_Only'].dt.day_name().str[:3]
 
128
 
129
- heatmap = alt.Chart(df_heatmap).mark_rect().encode(
130
- x=alt.X('Weekday_Name:N', sort=weekday_order, title='Day of Week'),
131
- y=alt.Y('Hour:O', title='Hour of Day'),
132
- color=alt.Color('count():Q', scale=alt.Scale(scheme='reds'), title='Crimes'),
133
- tooltip=['Weekday_Name', 'Hour', 'count()']
134
- ).properties(width=600, height=400)
 
 
 
 
 
135
 
136
- # 🌟 修复 3b:使用 width='stretch' 消除 Streamlit 警告
137
- st.altair_chart(heatmap, width='stretch')
 
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/)。")