Team Ai
Apppublic

ever-flow/visualization_modules

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
heatmap.py199 linesDownload Raw Back to visualizations
1import streamlit as st2import pandas as pd3import numpy as np4import plotly.graph_objects as go5 6from config import PERIODS7from aggregation import compute_aggregate8 9 10def show_heatmap(DF_RAW):11    st.header("EMSEC × EMTEC Heatmap")12    # —--- Sidebar Controls (unchanged) --------------------------------------13    with st.sidebar:14        st.markdown("**기준 연도**")15        year_sel = st.selectbox("", PERIODS, label_visibility="collapsed", key="year_sel1")16 17        st.markdown("**상장시장**")18        market_options = [19            "전체", "한국 전체", "KOSPI", "KOSDAQ", "미국 전체", "NASDAQ",20            "일본 전체", "Prime (Domestic Stocks)", "Standard (Domestic Stocks)", "Prime (Foreign Stocks)",21        ]22        market_sel = st.selectbox("", market_options, label_visibility="collapsed", key="market_sel1")23        country_filter = market_filter = None24        if "한국" in market_sel:25            country_filter = "한국"26        elif "미국" in market_sel:27            country_filter = "미국"28        elif "일본" in market_sel:29            country_filter = "일본"30        if market_sel not in ["전체", "한국 전체", "미국 전체", "일본 전체"]:31            market_filter = market_sel32 33        # Row (EMSEC)34        st.markdown("**Sector > Industry**")35        available_sectors = sorted([s for s in DF_RAW.Sector.unique() if pd.notna(s) and s != 'Unclassified'])36        sector_sel = st.selectbox("", ["전체"] + available_sectors, label_visibility="collapsed", key="sector_sel1")37        if sector_sel == "전체":38            industry_pool = sorted([i for i in DF_RAW.Industry.unique() if pd.notna(i) and i != 'Unclassified'])39        else:40            industry_pool = sorted([i for i in DF_RAW.loc[DF_RAW.Sector == sector_sel, "Industry"].unique() if pd.notna(i) and i != 'Unclassified'])41        industry_sel = st.selectbox("", ["전체"] + industry_pool, label_visibility="collapsed", key="industry_sel1")42 43        # Column (EMTEC)44        st.markdown("**Theme > Technology**")45        available_themes = sorted([t for t in DF_RAW.Theme.unique() if pd.notna(t) and t != 'Unclassified'])46        theme_sel = st.selectbox("", ["전체"] + available_themes, label_visibility="collapsed", key="theme_sel1")47        if theme_sel == "전체":48            tech_pool = sorted([t for t in DF_RAW.Technology.unique() if pd.notna(t) and t != 'Unclassified'])49        else:50            tech_pool = sorted([t for t in DF_RAW.loc[DF_RAW.Theme == theme_sel, "Technology"].unique() if pd.notna(t) and t != 'Unclassified'])51        tech_sel = st.selectbox("", ["전체"] + tech_pool, label_visibility="collapsed", key="tech_sel1")52 53        st.markdown("**계측값 선택**")54        group_sel = st.selectbox("", ["기업", "비교가치 멀티플", "재무비율"], label_visibility="collapsed", key="group_sel1")55        metric_main = metric_mode = base_col = agg_func = None56        allow_subtotal = True57        if group_sel == "기업":58            corp_first = st.selectbox("", ["기업수", "0이하 비율"], label_visibility="collapsed", key="corp_first1")59            if corp_first == "기업수":60                metric_main = "기업수"61                metric_mode = st.selectbox("", ["결측 포함", "결측 미포함", "결측 비율"], label_visibility="collapsed", key="metric_mode1")62            else:63                metric_main = "0이하비율"64                base_map = {"순이익": "Net_Income", "EBITDA": "EBITDA", "매출": "Sales", "자산총계": "Assets", "순자산": "Book"}65                base_sel = st.selectbox("", list(base_map.keys()), label_visibility="collapsed", key="corp_base1")66                base_col = base_map[base_sel]67                metric_mode = st.selectbox("", ["결측 포함", "결측 제외"], label_visibility="collapsed", key="metric_mode2")68        elif group_sel == "비교가치 멀티플":69            metric_main = st.selectbox("", ["PER", "PBR", "EV_EBITDA"], label_visibility="collapsed", key="metric_main1")70            agg_func = st.selectbox("", ["AVG", "HRM", "MED", "AGG"], label_visibility="collapsed", key="agg_func1")71            allow_subtotal = agg_func == "AGG"72        else:73            metric_main = st.selectbox("", ["ROE", "영업이익률", "EBITDA/Sales", "총자산이익률", "자산회전율", "자기자본비율", "부채비율", "시가총액/매출액", "시가총액/영업이익"], label_visibility="collapsed", key="metric_main2")74            agg_func = st.selectbox("", ["AVG", "HRM", "MED", "AGG"], label_visibility="collapsed", key="agg_func2")75            allow_subtotal = agg_func == "AGG"76 77    # —--- DataFrame Filtering ------------------------------------------------78    DF = DF_RAW[DF_RAW.Year == year_sel].copy()79    non_financial_keywords = ['EMSEC', 'EMTEC', 'ticker', 'market', 'Country', 'Market', 'name', 'Company']80    fin_cols = [c for c in DF.columns if not any(k in c for k in non_financial_keywords)]81    company_missing = DF.groupby('ticker')[fin_cols].apply(lambda x: x.isnull().values.any())82    DF['has_missing_financials'] = DF['ticker'].map(company_missing)83 84    if country_filter: DF = DF[DF.Country == country_filter]85    if market_filter: DF = DF[DF.Market == market_filter]86    if sector_sel != "전체": DF = DF[DF.Sector == sector_sel]87    if industry_sel != "전체": DF = DF[DF.Industry == industry_sel]88    if theme_sel != "전체": DF = DF[DF.Theme == theme_sel]89    if tech_sel != "전체": DF = DF[DF.Technology == tech_sel]90 91    # Index levels -----------------------------------------------------------92    row_index = "Sector" if sector_sel == "전체" else "Industry" if industry_sel == "전체" else "Sub_industry"93    col_index = "Theme" if theme_sel == "전체" else "Technology" if tech_sel == "전체" else "Sub_Technology"94 95    values_col = (base_col if group_sel == "기업" and metric_main == "0이하비율" else metric_main if group_sel != "기업" else "Company")96    if group_sel != "기업" and values_col not in DF.columns:97        st.warning(f"'{values_col}' 지표를 계산할 수 없습니다. 데이터나 설정을 확인해주세요.")98        st.stop()99    if DF.empty:100        st.warning("조건에 맞는 데이터가 없습니다.")101        st.stop()102 103    # Pivot (main values & counts) -------------------------------------------104    pivot_main = DF.groupby([row_index, col_index]).apply(105        lambda g: compute_aggregate(g, values_col, agg_func, year_sel, group_sel, metric_main, metric_mode, base_col)106    ).unstack(fill_value=np.nan)107    pivot_counts = DF.groupby([row_index, col_index])['Company'].nunique().unstack(fill_value=0)108 109    if pivot_main.empty:110        st.warning("피벗 테이블을 생성할 수 없습니다.")111        st.stop()112 113    x_orig, y_orig = pivot_main.columns.tolist(), pivot_main.index.tolist()114    z_core = pivot_main.values115    cnt_core = pivot_counts.reindex(index=y_orig, columns=x_orig).fillna(0).values116 117    # Subtotals/GrandTotals ---------------------------------------------------118    if allow_subtotal:119        row_tot = DF.groupby(row_index).apply(lambda g: compute_aggregate(g, values_col, agg_func, year_sel, group_sel, metric_main, metric_mode, base_col)).reindex(y_orig)120        col_tot = DF.groupby(col_index).apply(lambda g: compute_aggregate(g, values_col, agg_func, year_sel, group_sel, metric_main, metric_mode, base_col)).reindex(x_orig)121        grand_tot = compute_aggregate(DF, values_col, agg_func, year_sel, group_sel, metric_main, metric_mode, base_col)122        row_cnt = DF.groupby(row_index)['Company'].nunique().reindex(y_orig)123        col_cnt = DF.groupby(col_index)['Company'].nunique().reindex(x_orig)124        grand_cnt = DF['Company'].nunique()125 126        x_labels = ["Subtotal"] + x_orig127        y_labels = ["Subtotal"] + y_orig128        sz = (len(y_labels), len(x_labels))129        z_main, cnt_main = np.full(sz, np.nan), np.full(sz, 0)130        z_main[1:, 1:], cnt_main[1:, 1:] = z_core, cnt_core131        z_sub, cnt_sub = np.full(sz, np.nan), np.full(sz, 0)132        z_sub[0, 1:], z_sub[1:, 0] = col_tot.values, row_tot.values133        cnt_sub[0, 1:], cnt_sub[1:, 0] = col_cnt.values, row_cnt.values134        z_grd, cnt_grd = np.full(sz, np.nan), np.full(sz, 0)135        z_grd[0, 0], cnt_grd[0, 0] = grand_tot, grand_cnt136        z_comb = np.where(np.isnan(z_main), z_sub, z_main)137        z_comb[0, 0] = grand_tot138    else:139        x_labels, y_labels = x_orig, y_orig140        z_main, cnt_main = z_core, cnt_core141        z_comb, z_sub, z_grd, cnt_sub, cnt_grd = z_main, None, None, None, None142 143    # Value formatter ---------------------------------------------------------144    if group_sel == "기업":145        if metric_main == "기업수":146            fmt = (lambda v: f"{v:,}" if pd.notna(v) else "") if metric_mode in ["결측 포함", "결측 미포함"] else (lambda v: f"{v*100:.1f}%" if pd.notna(v) else "")147        else:148            fmt = lambda v: f"{v*100:.1f}%" if pd.notna(v) else ""149    elif metric_main in ["PER", "PBR", "EV_EBITDA", "시가총액/매출액", "시가총액/영업이익", "자산회전율"]:150        fmt = lambda v: f"{v:,.2f}x" if pd.notna(v) else ""151    elif metric_main in ["ROE", "영업이익률", "EBITDA/Sales", "총자산이익률", "자기자본비율", "부채비율"] and agg_func != 'AGG':152        fmt = lambda v: f"{v*100:.1f}%" if pd.notna(v) else ""153    else:154        fmt = lambda v: f"${v:,.0f}" if pd.notna(v) else ""155 156    txt = [[fmt(v) for v in row] for row in z_comb]157 158    # Plotly Heatmap ----------------------------------------------------------159    MAIN_CS = ["#e3f2fd", "#bbdefb", "#90caf9", "#64b5f6", "#42a5f5", "#2196f3", "#1e88e5", "#1976d2", "#1565c0", "#0d47a1"]160    SUB_CS = ["#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696", "#737373", "#525252"]161    GT_CS = [[0, "#000000"], [1, "#000000"]]162 163    fig = go.Figure()164    fig.add_trace(go.Heatmap(z=z_main, x=x_labels, y=y_labels, colorscale=MAIN_CS,165                             colorbar=dict(title=metric_main), customdata=cnt_main,166                             hovertemplate="%{y} / %{x}<br>값: %{z:,.3f}<br>기업수: %{customdata:,}", xgap=1, ygap=1, hoverongaps=False))167    if allow_subtotal:168        fig.add_trace(go.Heatmap(z=z_sub, x=x_labels, y=y_labels, colorscale=SUB_CS,169                                 showscale=False, customdata=cnt_sub,170                                 hovertemplate="%{y} / %{x}<br>Subtotal: %{z:,.3f}<br>기업수: %{customdata:,}", xgap=1, ygap=1, hoverongaps=False))171        fig.add_trace(go.Heatmap(z=z_grd, x=x_labels, y=y_labels, colorscale=GT_CS,172                                 showscale=False, customdata=cnt_grd,173                                 hovertemplate="Grand Total<br>값: %{z:,.3f}<br>기업수: %{customdata:,}", xgap=1, ygap=1, hoverongaps=False))174 175    # Annotations ------------------------------------------------------------176    annotations = []177    for r, row in enumerate(z_comb):178        for c, val in enumerate(row):179            if pd.isna(val):180                continue181            is_grand = allow_subtotal and r == 0 and c == 0182            color = "white" if is_grand else "black"183            annotations.append(dict(text=txt[r][c], x=x_labels[c], y=y_labels[r], showarrow=False, font=dict(color=color)))184 185    fig.update_layout(annotations=annotations,186                      height=max(650, 35*len(y_labels)),187                      margin=dict(l=40, r=40, t=40, b=40),188                      xaxis=dict(side="top", showgrid=False),189                      yaxis=dict(autorange="reversed", showgrid=False, categoryorder="array", categoryarray=y_labels),190                      showlegend=False)191 192    st.plotly_chart(fig, use_container_width=True)193 194    unique_companies = DF['Company'].nunique()195    total_combos = len(DF)196    st.caption(f"고유 기업 수: {unique_companies:,} | 총 분류 조합 수: {total_combos:,} | 절대값 지표: USD 기준")197 198 199