import streamlit as st
import pandas as pd
import requests
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import tempfile
import os
# 設置頁面配置
st.set_page_config(
page_title="碳排放數據可視化分析",
page_icon="🌱",
layout="wide",
initial_sidebar_state="expanded"
)
# 標題和介紹
st.title("🌱 碳排放數據可視化分析")
st.markdown("---")
st.write("此應用程式分析台灣公司的碳排放數據,包括範疇一和範疇二的排放量。")
# 側邊欄設置
st.sidebar.header("⚙️ 設置選項")
# 數據載入功能
@st.cache_data
def load_data():
"""載入並處理碳排放數據"""
try:
# 顯示載入狀態
with st.spinner("正在載入數據..."):
url = "https://mopsfin.twse.com.tw/opendata/t187ap46_O_1.csv"
response = requests.get(url)
# 使用臨時文件
with tempfile.NamedTemporaryFile(mode='wb', suffix='.csv', delete=False) as tmp_file:
tmp_file.write(response.content)
tmp_file_path = tmp_file.name
# 讀取CSV文件
df = pd.read_csv(tmp_file_path, encoding="utf-8-sig")
# 清理臨時文件
os.unlink(tmp_file_path)
# 數據清理
original_shape = df.shape
df = df.dropna()
# 尋找正確的欄位名稱
company_cols = [col for col in df.columns if "公司" in col or "代號" in col or "股票" in col]
emission_cols = [col for col in df.columns if "排放" in col]
# 自動識別欄位
company_col = "公司代號"
scope1_col = "範疇一排放量(公噸CO2e)"
scope2_col = "範疇二排放量(公噸CO2e)"
if company_col not in df.columns and company_cols:
company_col = company_cols[0]
if scope1_col not in df.columns:
scope1_candidates = [col for col in emission_cols if "範疇一" in col or "Scope1" in col]
if scope1_candidates:
scope1_col = scope1_candidates[0]
if scope2_col not in df.columns:
scope2_candidates = [col for col in emission_cols if "範疇二" in col or "Scope2" in col]
if scope2_candidates:
scope2_col = scope2_candidates[0]
# 轉換數值格式
if scope1_col in df.columns:
df[scope1_col] = pd.to_numeric(df[scope1_col], errors='coerce')
if scope2_col in df.columns:
df[scope2_col] = pd.to_numeric(df[scope2_col], errors='coerce')
# 移除轉換後的空值
available_cols = [col for col in [scope1_col, scope2_col, company_col] if col in df.columns]
df = df.dropna(subset=available_cols)
return df, original_shape, company_col, scope1_col, scope2_col, company_cols, emission_cols
except Exception as e:
st.error(f"載入數據時發生錯誤: {str(e)}")
return None, None, None, None, None, None, None
# 載入數據
data_result = load_data()
if data_result[0] is not None:
df, original_shape, company_col, scope1_col, scope2_col, company_cols, emission_cols = data_result
# 顯示數據基本信息
col1, col2, col3 = st.columns(3)
with col1:
st.metric("原始數據筆數", original_shape[0])
with col2:
st.metric("處理後數據筆數", df.shape[0])
with col3:
st.metric("總欄位數", df.shape[1])
# 側邊欄控制項
st.sidebar.subheader("📊 圖表選項")
# 圖表類型選擇
chart_types = st.sidebar.multiselect(
"選擇要顯示的圖表:",
["旭日圖", "雙層圓餅圖", "散點圖", "綜合旭日圖"],
default=["旭日圖", "雙層圓餅圖"]
)
# 公司數量選擇
max_companies = min(30, len(df))
num_companies = st.sidebar.slider(
"顯示公司數量:",
min_value=5,
max_value=max_companies,
value=min(15, max_companies),
step=5
)
# 顯示數據統計
if st.sidebar.checkbox("顯示數據統計", value=True):
st.subheader("📈 數據統計摘要")
if all(col in df.columns for col in [scope1_col, scope2_col]):
col1, col2 = st.columns(2)
with col1:
st.write("**範疇一排放量統計:**")
scope1_stats = df[scope1_col].describe()
st.write(f"- 平均值: {scope1_stats['mean']:.2f} 公噸CO2e")
st.write(f"- 中位數: {scope1_stats['50%']:.2f} 公噸CO2e")
st.write(f"- 最大值: {scope1_stats['max']:.2f} 公噸CO2e")
st.write(f"- 最小值: {scope1_stats['min']:.2f} 公噸CO2e")
with col2:
st.write("**範疇二排放量統計:**")
scope2_stats = df[scope2_col].describe()
st.write(f"- 平均值: {scope2_stats['mean']:.2f} 公噸CO2e")
st.write(f"- 中位數: {scope2_stats['50%']:.2f} 公噸CO2e")
st.write(f"- 最大值: {scope2_stats['max']:.2f} 公噸CO2e")
st.write(f"- 最小值: {scope2_stats['min']:.2f} 公噸CO2e")
# 圖表生成函數
def create_sunburst_chart(df, num_companies):
"""創建旭日圖"""
if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
df_top = df.nlargest(num_companies, scope1_col)
sunburst_data = []
for _, row in df_top.iterrows():
company = str(row[company_col])
scope1 = row[scope1_col]
scope2 = row[scope2_col]
sunburst_data.extend([
dict(ids=f"公司-{company}", labels=f"公司 {company}", parents="", values=scope1 + scope2),
dict(ids=f"範疇一-{company}", labels=f"範疇一: {scope1:.0f}", parents=f"公司-{company}", values=scope1),
dict(ids=f"範疇二-{company}", labels=f"範疇二: {scope2:.0f}", parents=f"公司-{company}", values=scope2)
])
fig_sunburst = go.Figure(go.Sunburst(
ids=[d['ids'] for d in sunburst_data],
labels=[d['labels'] for d in sunburst_data],
parents=[d['parents'] for d in sunburst_data],
values=[d['values'] for d in sunburst_data],
branchvalues="total",
hovertemplate='%{label}
排放量: %{value:.0f} 公噸CO2e',
maxdepth=3
))
fig_sunburst.update_layout(
title=f"碳排放量旭日圖 (前{num_companies}家公司)",
font_size=12,
height=600
)
return fig_sunburst
return None
def create_nested_pie_chart(df, num_companies):
"""創建雙層圓餅圖"""
if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
df_top = df.nlargest(num_companies, scope1_col)
fig = make_subplots(
rows=1, cols=2,
specs=[[{"type": "pie"}, {"type": "pie"}]],
subplot_titles=("範疇一排放量", "範疇二排放量")
)
fig.add_trace(go.Pie(
labels=df_top[company_col],
values=df_top[scope1_col],
name="範疇一",
hovertemplate='%{label}
範疇一排放量: %{value:.0f} 公噸CO2e
佔比: %{percent}',
textinfo='label+percent',
textposition='auto'
), row=1, col=1)
fig.add_trace(go.Pie(
labels=df_top[company_col],
values=df_top[scope2_col],
name="範疇二",
hovertemplate='%{label}
範疇二排放量: %{value:.0f} 公噸CO2e
佔比: %{percent}',
textinfo='label+percent',
textposition='auto'
), row=1, col=2)
fig.update_layout(
title_text=f"碳排放量圓餅圖比較 (前{num_companies}家公司)",
showlegend=True,
height=600
)
return fig
return None
def create_scatter_plot(df):
"""創建散點圖"""
if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
fig_scatter = px.scatter(
df,
x=scope1_col,
y=scope2_col,
hover_data=[company_col],
title="範疇一 vs 範疇二排放量散點圖",
labels={
scope1_col: "範疇一排放量 (公噸CO2e)",
scope2_col: "範疇二排放量 (公噸CO2e)"
},
hover_name=company_col
)
fig_scatter.update_layout(height=600)
return fig_scatter
return None
def create_comprehensive_sunburst(df, num_companies):
"""創建綜合旭日圖"""
if all(col in df.columns for col in [company_col, scope1_col, scope2_col]):
df_copy = df.copy()
df_copy['total_emission'] = df_copy[scope1_col] + df_copy[scope2_col]
df_copy['emission_level'] = pd.cut(df_copy['total_emission'],
bins=[0, 1000, 5000, 20000, float('inf')],
labels=['低排放(<1K)', '中排放(1K-5K)', '高排放(5K-20K)', '超高排放(>20K)'])
sunburst_data = []
companies_per_level = max(1, num_companies // 4)
for level in df_copy['emission_level'].unique():
if pd.isna(level):
continue
level_companies = df_copy[df_copy['emission_level'] == level].nlargest(companies_per_level, 'total_emission')
for _, row in level_companies.iterrows():
company = str(row[company_col])
scope1 = row[scope1_col]
scope2 = row[scope2_col]
total = scope1 + scope2
sunburst_data.extend([
dict(ids=str(level), labels=str(level), parents="", values=total),
dict(ids=f"{level}-{company}", labels=f"{company}", parents=str(level), values=total),
dict(ids=f"{level}-{company}-範疇一", labels=f"範疇一({scope1:.0f})",
parents=f"{level}-{company}", values=scope1),
dict(ids=f"{level}-{company}-範疇二", labels=f"範疇二({scope2:.0f})",
parents=f"{level}-{company}", values=scope2)
])
fig_comprehensive = go.Figure(go.Sunburst(
ids=[d['ids'] for d in sunburst_data],
labels=[d['labels'] for d in sunburst_data],
parents=[d['parents'] for d in sunburst_data],
values=[d['values'] for d in sunburst_data],
branchvalues="total",
hovertemplate='%{label}
排放量: %{value:.0f} 公噸CO2e',
maxdepth=4
))
fig_comprehensive.update_layout(
title="分級碳排放量旭日圖",
font_size=10,
height=700
)
return fig_comprehensive
return None
# 顯示選中的圖表
st.subheader("📊 互動式圖表")
if "旭日圖" in chart_types:
st.write("### 🌞 旭日圖")
fig1 = create_sunburst_chart(df, num_companies)
if fig1:
st.plotly_chart(fig1, use_container_width=True)
else:
st.error("無法創建旭日圖,缺少必要欄位")
if "雙層圓餅圖" in chart_types:
st.write("### 🥧 雙層圓餅圖")
fig2 = create_nested_pie_chart(df, num_companies)
if fig2:
st.plotly_chart(fig2, use_container_width=True)
else:
st.error("無法創建圓餅圖,缺少必要欄位")
if "散點圖" in chart_types:
st.write("### 📈 散點圖")
fig3 = create_scatter_plot(df)
if fig3:
st.plotly_chart(fig3, use_container_width=True)
else:
st.error("無法創建散點圖,缺少必要欄位")
if "綜合旭日圖" in chart_types:
st.write("### 🌟 綜合旭日圖")
fig4 = create_comprehensive_sunburst(df, num_companies)
if fig4:
st.plotly_chart(fig4, use_container_width=True)
else:
st.error("無法創建綜合旭日圖,缺少必要欄位")
# 顯示原始數據
if st.sidebar.checkbox("顯示原始數據"):
st.subheader("📋 原始數據預覽")
st.dataframe(df.head(100), use_container_width=True)
# 數據下載功能
if st.sidebar.button("下載處理後數據"):
csv = df.to_csv(index=False, encoding='utf-8-sig')
st.sidebar.download_button(
label="💾 下載 CSV 文件",
data=csv,
file_name="carbon_emission_data.csv",
mime="text/csv"
)
# 偵錯信息
if st.sidebar.checkbox("顯示偵錯信息"):
st.subheader("🔧 偵錯信息")
st.write("**識別的欄位:**")
st.write(f"- 公司欄位: {company_col}")
st.write(f"- 範疇一欄位: {scope1_col}")
st.write(f"- 範疇二欄位: {scope2_col}")
st.write("**所有可用欄位:**")
st.write(df.columns.tolist())
else:
st.error("無法載入數據,請檢查網路連接或數據源。")
# 頁面底部信息
st.markdown("---")
st.markdown(
"""
**數據來源:** 台灣證券交易所公開資訊觀測站
**更新時間:** 根據數據源自動更新
**製作:** Streamlit 碳排放數據分析應用
"""
)