CoolFace
Apppublic

Ryan181/AirPollution

sourceHugging Faceotherupdated 6mo agoView on Hugging Face
0likes
app.py338 linesDownload Raw Back to root
1import ssl2import urllib.request3import pandas as pd4import requests5import streamlit as st6import plotly.express as px7import plotly.graph_objects as go8 9# ── 頁面設定 ──────────────────────────────────────────────────────────────────10st.set_page_config(11    page_title="台南市開放資料分析",12    page_icon="📊",13    layout="wide",14)15 16# ── 自訂 CSS ──────────────────────────────────────────────────────────────────17st.markdown("""18<style>19    /* 主背景 */20    .stApp { background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); }21 22    /* 標題區塊 */23    .title-block {24        background: linear-gradient(90deg, #667eea, #764ba2);25        border-radius: 16px;26        padding: 28px 36px;27        margin-bottom: 24px;28        box-shadow: 0 8px 32px rgba(102,126,234,0.35);29    }30    .title-block h1 { color: #fff; font-size: 2.2rem; margin: 0; letter-spacing: 1px; }31    .title-block p  { color: rgba(255,255,255,0.75); margin: 6px 0 0; font-size: 1rem; }32 33    /* 指標卡片 */34    .metric-card {35        background: rgba(255,255,255,0.06);36        border: 1px solid rgba(255,255,255,0.12);37        border-radius: 14px;38        padding: 20px 24px;39        text-align: center;40        backdrop-filter: blur(8px);41    }42    .metric-card .val { font-size: 2rem; font-weight: 700; color: #a78bfa; }43    .metric-card .lbl { font-size: 0.85rem; color: rgba(255,255,255,0.6); margin-top: 4px; }44 45    /* 側邊欄 */46    section[data-testid="stSidebar"] {47        background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);48        border-right: 1px solid rgba(255,255,255,0.08);49    }50    section[data-testid="stSidebar"] * { color: #e2e8f0 !important; }51 52    /* 圖表容器 */53    .chart-container {54        background: rgba(255,255,255,0.04);55        border: 1px solid rgba(255,255,255,0.1);56        border-radius: 16px;57        padding: 8px;58        margin-top: 8px;59    }60 61    /* Divider */62    hr { border-color: rgba(255,255,255,0.1); }63</style>64""", unsafe_allow_html=True)65 66# ── 資料載入(含 SSL 略過) ────────────────────────────────────────────────────67@st.cache_data(show_spinner=False)68def load_data():69    url = "https://data.tainan.gov.tw/File/ResourceCsvDownload/aad7b580-5a86-467d-8d91-fda661267a71"70    ctx = ssl.create_default_context()71    ctx.check_hostname = False72    ctx.verify_mode = ssl.CERT_NONE73    try:74        with urllib.request.urlopen(url, context=ctx) as resp:75            raw = resp.read()76    except Exception:77        # Fallback: requests 不驗憑證78        r = requests.get(url, verify=False, timeout=30)79        raw = r.content80    import io81    df = pd.read_csv(io.BytesIO(raw), encoding="utf-8-sig")82    df = df.dropna()83    df.columns = df.columns.str.strip()84    return df85 86# ── 讀取資料 ──────────────────────────────────────────────────────────────────87with st.spinner("📡 正在載入資料…"):88    try:89        df_raw = load_data()90        load_ok = True91    except Exception as e:92        load_ok = False93        err_msg = str(e)94 95# ── 標題 ──────────────────────────────────────────────────────────────────────96st.markdown("""97<div class="title-block">98    <h1>📊 台南市開放資料分析儀表板</h1>99    <p>互動式旭日圖 &amp; 折線圖 · Powered by Streamlit × Plotly</p>100</div>101""", unsafe_allow_html=True)102 103if not load_ok:104    st.error(f"資料載入失敗:{err_msg}")105    st.stop()106 107# ── 欄位偵測 ──────────────────────────────────────────────────────────────────108cols = df_raw.columns.tolist()109 110# 自動偵測「縣市」欄(優先關鍵字)111CITY_KEYS   = ["縣市", "city", "行政區", "地區", "區域", "發生地點"]112ADDR_KEYS   = ["地址類型", "道路類型", "路段", "地點", "type", "類型", "肇事地點"]113TIME_KEYS   = ["年", "year", "月", "month", "日期", "發生時間", "時間"]114COUNT_KEYS  = ["件數", "數量", "count", "筆數", "總數", "死亡人數", "受傷人數"]115 116def auto_col(keys):117    for k in keys:118        for c in cols:119            if k.lower() in c.lower():120                return c121    return None122 123city_col  = auto_col(CITY_KEYS)124addr_col  = auto_col(ADDR_KEYS)125time_col  = auto_col(TIME_KEYS)126count_col = auto_col(COUNT_KEYS)127 128# ── 側邊欄:篩選器 ────────────────────────────────────────────────────────────129with st.sidebar:130    st.markdown("## ⚙️ 篩選條件")131    st.markdown("---")132 133    # 縣市欄位選擇134    city_col_sel = st.selectbox(135        "縣市 / 地點欄位",136        options=cols,137        index=cols.index(city_col) if city_col else 0,138    )139 140    # 使用者輸入縣市關鍵字141    city_input = st.text_input("🔍 輸入縣市(模糊搜尋,留空=全部)", placeholder="例:台南、臺南、永康")142 143    st.markdown("---")144 145    # 地址類型欄位選擇146    addr_col_sel = st.selectbox(147        "地址類型欄位",148        options=cols,149        index=cols.index(addr_col) if addr_col else 0,150    )151 152    # 複選地址類型153    all_addr_types = sorted(df_raw[addr_col_sel].dropna().unique().tolist())154    selected_addr = st.multiselect(155        "📍 地址類型(複選)",156        options=all_addr_types,157        default=all_addr_types[:min(5, len(all_addr_types))],158    )159 160    st.markdown("---")161 162    # 時間欄 & 數值欄163    time_col_sel = st.selectbox(164        "時間欄位(折線圖 X 軸)",165        options=[None] + cols,166        index=(cols.index(time_col) + 1) if time_col else 0,167        format_func=lambda x: "(自動計數)" if x is None else x,168    )169    count_col_sel = st.selectbox(170        "數值欄位(折線圖 Y 軸)",171        options=[None] + cols,172        index=(cols.index(count_col) + 1) if count_col else 0,173        format_func=lambda x: "(計筆數)" if x is None else x,174    )175 176    st.markdown("---")177    st.caption("資料來源:台南市政府開放資料平台")178 179# ── 篩選資料 ──────────────────────────────────────────────────────────────────180df = df_raw.copy()181 182if city_input.strip():183    df = df[df[city_col_sel].astype(str).str.contains(city_input.strip(), na=False)]184 185if selected_addr:186    df = df[df[addr_col_sel].isin(selected_addr)]187 188# ── 指標列 ────────────────────────────────────────────────────────────────────189total_rows  = len(df)190addr_count  = df[addr_col_sel].nunique()191city_count  = df[city_col_sel].nunique()192 193c1, c2, c3, c4 = st.columns(4)194for col_ui, val, lbl in zip(195    [c1, c2, c3, c4],196    [len(df_raw), total_rows, city_count, addr_count],197    ["原始筆數", "篩選後筆數", "縣市種類數", "地址類型數"],198):199    col_ui.markdown(f"""200    <div class="metric-card">201        <div class="val">{val:,}</div>202        <div class="lbl">{lbl}</div>203    </div>204    """, unsafe_allow_html=True)205 206st.markdown("<br>", unsafe_allow_html=True)207 208# ── 若無資料 ──────────────────────────────────────────────────────────────────209if df.empty:210    st.warning("⚠️ 篩選條件下無資料,請調整左側設定。")211    st.stop()212 213# ── 圖表區:旭日圖 & 折線圖 ───────────────────────────────────────────────────214left, right = st.columns([1, 1], gap="large")215 216# ╔══════════ 旭日圖 ════════════╗217with left:218    st.markdown("### 🌞 旭日圖")219    st.caption(f"以「{city_col_sel}」→「{addr_col_sel}」雙層展開")220 221    # 計算各組合筆數222    sun_df = (223        df.groupby([city_col_sel, addr_col_sel])224        .size()225        .reset_index(name="件數")226    )227 228    fig_sun = px.sunburst(229        sun_df,230        path=[city_col_sel, addr_col_sel],231        values="件數",232        color="件數",233        color_continuous_scale="Plasma",234        title="",235    )236    fig_sun.update_traces(237        textinfo="label+percent root",238        hovertemplate="<b>%{label}</b><br>件數:%{value}<br>佔比:%{percentRoot:.1%}<extra></extra>",239    )240    fig_sun.update_layout(241        paper_bgcolor="rgba(0,0,0,0)",242        plot_bgcolor="rgba(0,0,0,0)",243        font_color="#e2e8f0",244        height=520,245        margin=dict(t=20, b=10, l=10, r=10),246        coloraxis_colorbar=dict(247            tickfont=dict(color="#e2e8f0"),248            title=dict(text="件數", font=dict(color="#e2e8f0")),249        ),250    )251    with st.container():252        st.markdown('<div class="chart-container">', unsafe_allow_html=True)253        st.plotly_chart(fig_sun, use_container_width=True)254        st.markdown('</div>', unsafe_allow_html=True)255 256# ╔══════════ 折線圖 ════════════╗257with right:258    st.markdown("### 📈 折線圖")259 260    if time_col_sel:261        st.caption(f"X 軸:{time_col_sel} Y 軸:{'筆數' if not count_col_sel else count_col_sel}")262        if count_col_sel:263            line_df = (264                df.groupby([time_col_sel, addr_col_sel])[count_col_sel]265                .sum()266                .reset_index()267                .rename(columns={count_col_sel: "數值"})268            )269            y_label = count_col_sel270        else:271            line_df = (272                df.groupby([time_col_sel, addr_col_sel])273                .size()274                .reset_index(name="數值")275            )276            y_label = "筆數"277 278        line_df[time_col_sel] = line_df[time_col_sel].astype(str)279 280        fig_line = px.line(281            line_df,282            x=time_col_sel,283            y="數值",284            color=addr_col_sel,285            markers=True,286            labels={"數值": y_label, time_col_sel: time_col_sel, addr_col_sel: "地址類型"},287            color_discrete_sequence=px.colors.qualitative.Vivid,288        )289    else:290        # 無時間欄:用地址類型做橫軸長條折線291        st.caption("(未指定時間欄,改以地址類型為 X 軸)")292        line_df = (293            df.groupby([addr_col_sel])294            .size()295            .reset_index(name="筆數")296            .sort_values("筆數", ascending=False)297        )298        fig_line = go.Figure(299            go.Scatter(300                x=line_df[addr_col_sel].astype(str),301                y=line_df["筆數"],302                mode="lines+markers",303                marker=dict(size=10, color="#a78bfa"),304                line=dict(color="#7c3aed", width=2.5),305                hovertemplate="%{x}<br>筆數:%{y}<extra></extra>",306            )307        )308        fig_line.update_layout(xaxis_title="地址類型", yaxis_title="筆數")309 310    fig_line.update_layout(311        paper_bgcolor="rgba(0,0,0,0)",312        plot_bgcolor="rgba(0,0,0,0)",313        font_color="#e2e8f0",314        height=520,315        margin=dict(t=20, b=10, l=10, r=10),316        legend=dict(317            bgcolor="rgba(255,255,255,0.05)",318            bordercolor="rgba(255,255,255,0.1)",319            font=dict(color="#e2e8f0"),320        ),321        xaxis=dict(gridcolor="rgba(255,255,255,0.08)", tickangle=-30),322        yaxis=dict(gridcolor="rgba(255,255,255,0.08)"),323        hovermode="x unified",324    )325    with st.container():326        st.markdown('<div class="chart-container">', unsafe_allow_html=True)327        st.plotly_chart(fig_line, use_container_width=True)328        st.markdown('</div>', unsafe_allow_html=True)329 330# ── 原始資料預覽 ──────────────────────────────────────────────────────────────331st.markdown("---")332with st.expander("🗂️ 查看篩選後原始資料", expanded=False):333    st.dataframe(334        df.reset_index(drop=True),335        use_container_width=True,336        height=300,337    )338    st.caption(f"共 {len(df):,} 筆 · 欄位:{', '.join(df.columns.tolist())}")