Rooobert commited on
Commit
0fde500
·
verified ·
1 Parent(s): ba4585d

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +356 -32
src/streamlit_app.py CHANGED
@@ -1,40 +1,364 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
 
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
 
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import requests
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+ import numpy as np
8
+ import tempfile
9
+ import os
10
 
11
+ # 設置頁面配置
12
+ st.set_page_config(
13
+ page_title="碳排放數據可視化分析",
14
+ page_icon="🌱",
15
+ layout="wide",
16
+ initial_sidebar_state="expanded"
17
+ )
18
 
19
+ # 標題和介紹
20
+ st.title("🌱 碳排放數據可視化分析")
21
+ st.markdown("---")
22
+ st.write("此應用程式分析台灣公司的碳排放數據,包括範疇一和範疇二的排放量。")
23
 
24
+ # 側邊欄設置
25
+ st.sidebar.header("⚙️ 設置選項")
26
 
27
+ # 數據載入功能
28
+ @st.cache_data
29
+ def load_data():
30
+ """載入並處理碳排放數據"""
31
+ try:
32
+ # 顯示載入狀態
33
+ with st.spinner("正在載入數據..."):
34
+ url = "https://mopsfin.twse.com.tw/opendata/t187ap46_O_1.csv"
35
+ response = requests.get(url)
36
+
37
+ # 使用臨時文件
38
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.csv', delete=False) as tmp_file:
39
+ tmp_file.write(response.content)
40
+ tmp_file_path = tmp_file.name
41
+
42
+ # 讀取CSV文件
43
+ df = pd.read_csv(tmp_file_path, encoding="utf-8-sig")
44
+
45
+ # 清理臨時文件
46
+ os.unlink(tmp_file_path)
47
+
48
+ # 數據清理
49
+ original_shape = df.shape
50
+ df = df.dropna()
51
+
52
+ # 尋找正確的欄位名稱
53
+ company_cols = [col for col in df.columns if "公司" in col or "代號" in col or "股票" in col]
54
+ emission_cols = [col for col in df.columns if "排放" in col]
55
+
56
+ # 自動識別欄位
57
+ company_col = "公司代號"
58
+ scope1_col = "範疇一排放量(公噸CO2e)"
59
+ scope2_col = "範疇二排放量(公噸CO2e)"
60
+
61
+ if company_col not in df.columns and company_cols:
62
+ company_col = company_cols[0]
63
+
64
+ if scope1_col not in df.columns:
65
+ scope1_candidates = [col for col in emission_cols if "範疇一" in col or "Scope1" in col]
66
+ if scope1_candidates:
67
+ scope1_col = scope1_candidates[0]
68
+
69
+ if scope2_col not in df.columns:
70
+ scope2_candidates = [col for col in emission_cols if "範疇二" in col or "Scope2" in col]
71
+ if scope2_candidates:
72
+ scope2_col = scope2_candidates[0]
73
+
74
+ # 轉換數值格式
75
+ if scope1_col in df.columns:
76
+ df[scope1_col] = pd.to_numeric(df[scope1_col], errors='coerce')
77
+ if scope2_col in df.columns:
78
+ df[scope2_col] = pd.to_numeric(df[scope2_col], errors='coerce')
79
+
80
+ # 移除轉換後的空值
81
+ available_cols = [col for col in [scope1_col, scope2_col, company_col] if col in df.columns]
82
+ df = df.dropna(subset=available_cols)
83
+
84
+ return df, original_shape, company_col, scope1_col, scope2_col, company_cols, emission_cols
85
+
86
+ except Exception as e:
87
+ st.error(f"載入數據時發生錯誤: {str(e)}")
88
+ return None, None, None, None, None, None, None
89
 
90
+ # 載入數據
91
+ data_result = load_data()
92
+ if data_result[0] is not None:
93
+ df, original_shape, company_col, scope1_col, scope2_col, company_cols, emission_cols = data_result
94
+
95
+ # 顯示數據基本信息
96
+ col1, col2, col3 = st.columns(3)
97
+ with col1:
98
+ st.metric("原始數據筆數", original_shape[0])
99
+ with col2:
100
+ st.metric("處理後數據筆數", df.shape[0])
101
+ with col3:
102
+ st.metric("總欄位數", df.shape[1])
103
+
104
+ # 側邊欄控制項
105
+ st.sidebar.subheader("📊 圖表選項")
106
+
107
+ # 圖表類型選擇
108
+ chart_types = st.sidebar.multiselect(
109
+ "選擇要顯示的圖表:",
110
+ ["旭日圖", "雙層圓餅圖", "散點圖", "綜合旭日圖"],
111
+ default=["旭日圖", "雙層圓餅圖"]
112
+ )
113
+
114
+ # 公司數量選擇
115
+ max_companies = min(30, len(df))
116
+ num_companies = st.sidebar.slider(
117
+ "顯示公司數量:",
118
+ min_value=5,
119
+ max_value=max_companies,
120
+ value=min(15, max_companies),
121
+ step=5
122
+ )
123
+
124
+ # 顯示數據統計
125
+ if st.sidebar.checkbox("顯示數據統計", value=True):
126
+ st.subheader("📈 數據統計摘要")
127
+
128
+ if all(col in df.columns for col in [scope1_col, scope2_col]):
129
+ col1, col2 = st.columns(2)
130
+
131
+ with col1:
132
+ st.write("**範疇一排放量統計:**")
133
+ scope1_stats = df[scope1_col].describe()
134
+ st.write(f"- 平均值: {scope1_stats['mean']:.2f} 公噸CO2e")
135
+ st.write(f"- 中位數: {scope1_stats['50%']:.2f} 公噸CO2e")
136
+ st.write(f"- 最大值: {scope1_stats['max']:.2f} 公噸CO2e")
137
+ st.write(f"- 最小值: {scope1_stats['min']:.2f} 公噸CO2e")
138
+
139
+ with col2:
140
+ st.write("**範疇二排放量統計:**")
141
+ scope2_stats = df[scope2_col].describe()
142
+ st.write(f"- 平均值: {scope2_stats['mean']:.2f} 公噸CO2e")
143
+ st.write(f"- 中位數: {scope2_stats['50%']:.2f} 公噸CO2e")
144
+ st.write(f"- 最大值: {scope2_stats['max']:.2f} 公噸CO2e")
145
+ st.write(f"- 最小值: {scope2_stats['min']:.2f} 公噸CO2e")
146
+
147
+ # 圖表生成函數
148
+ def create_sunburst_chart(df, num_companies):
149
+ """創建旭日圖"""
150
+ if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
151
+ df_top = df.nlargest(num_companies, scope1_col)
152
+ sunburst_data = []
153
+
154
+ for _, row in df_top.iterrows():
155
+ company = str(row[company_col])
156
+ scope1 = row[scope1_col]
157
+ scope2 = row[scope2_col]
158
+
159
+ sunburst_data.extend([
160
+ dict(ids=f"公司-{company}", labels=f"公司 {company}", parents="", values=scope1 + scope2),
161
+ dict(ids=f"範疇一-{company}", labels=f"範疇一: {scope1:.0f}", parents=f"公司-{company}", values=scope1),
162
+ dict(ids=f"範疇二-{company}", labels=f"範疇二: {scope2:.0f}", parents=f"公司-{company}", values=scope2)
163
+ ])
164
+
165
+ fig_sunburst = go.Figure(go.Sunburst(
166
+ ids=[d['ids'] for d in sunburst_data],
167
+ labels=[d['labels'] for d in sunburst_data],
168
+ parents=[d['parents'] for d in sunburst_data],
169
+ values=[d['values'] for d in sunburst_data],
170
+ branchvalues="total",
171
+ hovertemplate='<b>%{label}</b><br>排放量: %{value:.0f} 公噸CO2e<extra></extra>',
172
+ maxdepth=3
173
+ ))
174
+
175
+ fig_sunburst.update_layout(
176
+ title=f"碳排放量旭日圖 (前{num_companies}家公司)",
177
+ font_size=12,
178
+ height=600
179
+ )
180
+
181
+ return fig_sunburst
182
+ return None
183
+
184
+ def create_nested_pie_chart(df, num_companies):
185
+ """創建雙層圓餅圖"""
186
+ if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
187
+ df_top = df.nlargest(num_companies, scope1_col)
188
+
189
+ fig = make_subplots(
190
+ rows=1, cols=2,
191
+ specs=[[{"type": "pie"}, {"type": "pie"}]],
192
+ subplot_titles=("範疇一排放量", "範疇二排放量")
193
+ )
194
+
195
+ fig.add_trace(go.Pie(
196
+ labels=df_top[company_col],
197
+ values=df_top[scope1_col],
198
+ name="範疇一",
199
+ hovertemplate='<b>%{label}</b><br>範疇一排放量: %{value:.0f} 公噸CO2e<br>佔比: %{percent}<extra></extra>',
200
+ textinfo='label+percent',
201
+ textposition='auto'
202
+ ), row=1, col=1)
203
+
204
+ fig.add_trace(go.Pie(
205
+ labels=df_top[company_col],
206
+ values=df_top[scope2_col],
207
+ name="範疇二",
208
+ hovertemplate='<b>%{label}</b><br>範疇二排放量: %{value:.0f} 公噸CO2e<br>佔比: %{percent}<extra></extra>',
209
+ textinfo='label+percent',
210
+ textposition='auto'
211
+ ), row=1, col=2)
212
+
213
+ fig.update_layout(
214
+ title_text=f"碳排放量圓餅圖比較 (前{num_companies}家公司)",
215
+ showlegend=True,
216
+ height=600
217
+ )
218
+
219
+ return fig
220
+ return None
221
+
222
+ def create_scatter_plot(df):
223
+ """創建散點圖"""
224
+ if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
225
+ fig_scatter = px.scatter(
226
+ df,
227
+ x=scope1_col,
228
+ y=scope2_col,
229
+ hover_data=[company_col],
230
+ title="範疇一 vs 範疇二排放量散點圖",
231
+ labels={
232
+ scope1_col: "範疇一排放量 (公噸CO2e)",
233
+ scope2_col: "範疇二排放量 (公噸CO2e)"
234
+ },
235
+ hover_name=company_col
236
+ )
237
+
238
+ fig_scatter.update_layout(height=600)
239
+ return fig_scatter
240
+ return None
241
+
242
+ def create_comprehensive_sunburst(df, num_companies):
243
+ """創建綜合旭日圖"""
244
+ if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
245
+ df_copy = df.copy()
246
+ df_copy['total_emission'] = df_copy[scope1_col] + df_copy[scope2_col]
247
+ df_copy['emission_level'] = pd.cut(df_copy['total_emission'],
248
+ bins=[0, 1000, 5000, 20000, float('inf')],
249
+ labels=['低排放(<1K)', '中排放(1K-5K)', '高排放(5K-20K)', '超高排放(>20K)'])
250
+
251
+ sunburst_data = []
252
+ companies_per_level = max(1, num_companies // 4)
253
+
254
+ for level in df_copy['emission_level'].unique():
255
+ if pd.isna(level):
256
+ continue
257
+ level_companies = df_copy[df_copy['emission_level'] == level].nlargest(companies_per_level, 'total_emission')
258
+
259
+ for _, row in level_companies.iterrows():
260
+ company = str(row[company_col])
261
+ scope1 = row[scope1_col]
262
+ scope2 = row[scope2_col]
263
+ total = scope1 + scope2
264
+
265
+ sunburst_data.extend([
266
+ dict(ids=str(level), labels=str(level), parents="", values=total),
267
+ dict(ids=f"{level}-{company}", labels=f"{company}", parents=str(level), values=total),
268
+ dict(ids=f"{level}-{company}-範疇一", labels=f"範疇一({scope1:.0f})",
269
+ parents=f"{level}-{company}", values=scope1),
270
+ dict(ids=f"{level}-{company}-範疇二", labels=f"範疇二({scope2:.0f})",
271
+ parents=f"{level}-{company}", values=scope2)
272
+ ])
273
+
274
+ fig_comprehensive = go.Figure(go.Sunburst(
275
+ ids=[d['ids'] for d in sunburst_data],
276
+ labels=[d['labels'] for d in sunburst_data],
277
+ parents=[d['parents'] for d in sunburst_data],
278
+ values=[d['values'] for d in sunburst_data],
279
+ branchvalues="total",
280
+ hovertemplate='<b>%{label}</b><br>排放量: %{value:.0f} 公噸CO2e<extra></extra>',
281
+ maxdepth=4
282
+ ))
283
+
284
+ fig_comprehensive.update_layout(
285
+ title="分級碳排放量旭日圖",
286
+ font_size=10,
287
+ height=700
288
+ )
289
+
290
+ return fig_comprehensive
291
+ return None
292
+
293
+ # 顯示選中的圖表
294
+ st.subheader("📊 互動式圖表")
295
+
296
+ if "旭日圖" in chart_types:
297
+ st.write("### 🌞 旭日圖")
298
+ fig1 = create_sunburst_chart(df, num_companies)
299
+ if fig1:
300
+ st.plotly_chart(fig1, use_container_width=True)
301
+ else:
302
+ st.error("無法創建旭日圖,缺少必要欄位")
303
+
304
+ if "雙層圓餅圖" in chart_types:
305
+ st.write("### 🥧 雙層圓餅圖")
306
+ fig2 = create_nested_pie_chart(df, num_companies)
307
+ if fig2:
308
+ st.plotly_chart(fig2, use_container_width=True)
309
+ else:
310
+ st.error("無法創建圓餅圖,缺少必要欄位")
311
+
312
+ if "散點圖" in chart_types:
313
+ st.write("### 📈 散點圖")
314
+ fig3 = create_scatter_plot(df)
315
+ if fig3:
316
+ st.plotly_chart(fig3, use_container_width=True)
317
+ else:
318
+ st.error("無法創建散點圖,缺少必要欄位")
319
+
320
+ if "綜合旭日圖" in chart_types:
321
+ st.write("### 🌟 綜合旭日圖")
322
+ fig4 = create_comprehensive_sunburst(df, num_companies)
323
+ if fig4:
324
+ st.plotly_chart(fig4, use_container_width=True)
325
+ else:
326
+ st.error("無法創建綜合旭日圖,缺少必要欄位")
327
+
328
+ # 顯示原始數據
329
+ if st.sidebar.checkbox("顯示原始數據"):
330
+ st.subheader("📋 原始數據預覽")
331
+ st.dataframe(df.head(100), use_container_width=True)
332
+
333
+ # 數據下載功能
334
+ if st.sidebar.button("下載處理後數據"):
335
+ csv = df.to_csv(index=False, encoding='utf-8-sig')
336
+ st.sidebar.download_button(
337
+ label="💾 下載 CSV 文件",
338
+ data=csv,
339
+ file_name="carbon_emission_data.csv",
340
+ mime="text/csv"
341
+ )
342
+
343
+ # 偵錯信息
344
+ if st.sidebar.checkbox("顯示偵錯信息"):
345
+ st.subheader("🔧 偵錯信息")
346
+ st.write("**識別的欄位:**")
347
+ st.write(f"- 公司欄位: {company_col}")
348
+ st.write(f"- 範疇一欄位: {scope1_col}")
349
+ st.write(f"- 範疇二欄位: {scope2_col}")
350
+ st.write("**所有可用欄位:**")
351
+ st.write(df.columns.tolist())
352
 
353
+ else:
354
+ st.error("無法載入數據,請檢查網路連接或數據源。")
 
 
 
 
355
 
356
+ # 頁面底部信息
357
+ st.markdown("---")
358
+ st.markdown(
359
+ """
360
+ **數據來源:** 台灣證券交易所公開資訊觀測站
361
+ **更新時間:** 根據數據源自動更新
362
+ **製作:** Streamlit 碳排放數據分析應用
363
+ """
364
+ )