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 碳排放數據分析應用 """ )