Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- 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 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
12 |
|
13 |
-
|
14 |
-
""
|
|
|
|
|
15 |
|
16 |
-
|
17 |
-
|
18 |
|
19 |
-
|
20 |
-
|
21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22 |
|
23 |
-
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
-
|
27 |
-
"
|
28 |
-
"y": y,
|
29 |
-
"idx": indices,
|
30 |
-
"rand": np.random.randn(num_points),
|
31 |
-
})
|
32 |
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
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 |
+
)
|