26J30k5

Young-Kyoo Kim·5일 전
"""
step3_analytics.py — 장기 시계열 인프라 데이터 시각화 및 20대 마스터 한글 차트 드로잉 엔진
"""
import os
import re
import pandas as pd
import numpy as np
from pathlib import Path

# ─── 🛡️ [클라우드 네이티브 가드] GUI 디스플레이 서버가 없는 K8s Pod 환경 크래시 방지 ───
import matplotlib
matplotlib.use('Agg') 
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns

# ─── 📂 [경로 동기화] ./data 상대 경로 표준화 ───
BASE_DATA_DIR = Path("./data")
MERGED_DIR    = BASE_DATA_DIR / "merged"
PLOT_DIR      = BASE_DATA_DIR / "output" / "plots"

# 차트 디렉토리 생성 보장
PLOT_DIR.mkdir(parents=True, exist_ok=True)

# ─── 🔤 [핵심 해결] 멀티 OS 호환형 엔터프라이즈 한글 폰트 자동 빌더 ───
print("⚙️ 시스템 내장 한글 폰트 엔진 탐색 및 등록 중...")
# 운영 컨테이너(나눔), 윈도우(맑은), 맥(애플) 서열 매핑
target_korean_fonts = ["NanumGothic", "NanumBarunGothic", "Malgun Gothic", "AppleGothic", "DejaVu Sans"]
available_system_fonts = [f.name for f in fm.font_manager.ttflist]

matched_font = "sans-serif"
for font_name in target_korean_fonts:
    if font_name in available_system_fonts:
        matched_font = font_name
        break

# Matplotlib 글로벌 컨텍스트 패치 적용
plt.rcParams['font.family'] = matched_font
plt.rcParams['axes.unicode_minus'] = False # 음수 기호 마이너스( - ) 깨짐 방지 가드레일
sns.set_theme(style="whitegrid", font=matched_font)
print(f"✅ 폰트 바인딩 완수 ➡️ 현재 차트 가동 폰트: [{matched_font}]")

def check_empty_data(df, chart_name):
    if df.empty:
        print(f"  ⚠️  [데이터 공백] {chart_name}을(를) 그릴 로우가 없어 빈 차트로 대체합니다.")
        return True
    return False

def main():
    print("\n🚀 [Step3 개시] FinOps 빅데이터 시계열 시각화 한글 차트 분석 엔진 가동...")
    
    p1 = MERGED_DIR / "enriched_fixed_7d.parquet"
    p2 = MERGED_DIR / "pareto_fixed_ns.parquet"
    
    if not p1.exists():
        print("❌ 오류: step2 파이프라인의 가공 산출물(Parquet)이 없습니다. step2를 먼저 실행하세요.")
        return
        
    df_pod = pd.read_parquet(p1)
    df_ns  = pd.read_parquet(p2) if p2.exists() else pd.DataFrame()
    print(f"✅ 데이터 레이크 로드 완료 -> 분석 대상 컨테이너 세트: {len(df_pod):,}행 스캔 성공.\n")

    # 활용률(Utilization) 계산 레이어 주입
    df_pod["cpu_util"] = np.where(df_pod["cpu_request_max"] > 0, (df_pod["cpu_usage_p95"] / df_pod["cpu_request_max"] * 100), 0)
    df_pod["mem_util"] = np.where(df_pod["mem_request_max"] > 0, (df_pod["mem_usage_p95"] / df_pod["mem_request_max"] * 100), 0)
    df_pod["lim_req_ratio"] = np.where(df_pod["cpu_request_max"] > 0, df_pod["cpu_limit_max"] / df_pod["cpu_request_max"], 0)

    # ─── 📊 [차트 1 & 2] 워크로드 타입별 자원 할당 vs 실사용 피크 분석 ───
    print("⏳ [1/19] chart1_cpu_req_vs_usage_by_workload 시각화 연산 중...")
    df_wl_cpu = df_pod.groupby("workload_type")[["cpu_request_max", "cpu_usage_p95"]].mean().reset_index()
    plt.figure(figsize=(10, 5))
    df_melt_cpu = df_wl_cpu.melt(id_vars="workload_type", value_vars=["cpu_request_max", "cpu_usage_p95"])
    sns.barplot(data=df_melt_cpu, x="workload_type", y="value", hue="variable", palette="Blues_r")
    plt.xticks(rotation=30, ha='right')
    plt.title("워크로드 타입별 평균 CPU Request 사양 vs P95 Peak 사용량 비교")
    plt.xlabel("워크로드 분류")
    plt.ylabel("CPU 코어 수 (Cores)")
    plt.tight_layout()
    out1 = PLOT_DIR / "chart1_cpu_req_vs_usage_by_workload.png"
    plt.savefig(out1, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out1.name}")

    print("⏳ [2/19] chart2_mem_req_vs_usage_by_workload 시각화 연산 중...")
    df_wl_mem = df_pod.groupby("workload_type")[["mem_request_max", "mem_usage_p95"]].mean().reset_index()
    plt.figure(figsize=(10, 5))
    df_melt_mem = df_wl_mem.melt(id_vars="workload_type", value_vars=["mem_request_max", "mem_usage_p95"])
    sns.barplot(data=df_melt_mem, x="workload_type", y="value", hue="variable", palette="Purples_r")
    plt.xticks(rotation=30, ha='right')
    plt.title("워크로드 타입별 평균 Memory Request 사양 vs P95 Peak 사용량 (GB) 비교")
    plt.xlabel("워크로드 분류")
    plt.ylabel("메모리 크기 (GB)")
    plt.tight_layout()
    out2 = PLOT_DIR / "chart2_mem_req_vs_usage_by_workload.png"
    plt.savefig(out2, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out2.name}")

    # ─── 📊 [차트 3] 일별 CPU 낭비 누적 막대 차트 ───
    print("⏳ [3/19] chart3_daily_waste_stack 누적 시계열 연산 중...")
    df_daily_waste = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0)
    if not check_empty_data(df_daily_waste, "chart3_daily_waste_stack"):
        df_daily_waste.plot(kind='bar', stacked=True, figsize=(11, 5), cmap="tab20")
        plt.title("기술 도메인별 일별 CPU 자원 낭비 손실 누적 추이 (KST)")
        plt.xlabel("관측 일자 (KST)")
        plt.ylabel("누적 낭비량 (Core-Hours)")
        plt.xticks(rotation=45)
        plt.tight_layout()
        out3 = PLOT_DIR / "chart3_daily_waste_stack.png"
        plt.savefig(out3, dpi=100)
        plt.close()
        print(f"  -> 🎨 차트 렌더링 완료: {out3.name}")

    # ─── 📊 [차트 4 & 18] 자원 활용 지형 히트맵 (Heatmap) 연산 ───
    print("⏳ [4/19] chart4_cpu_efficiency_heatmap 격자 분석 중...")
    df_heat_cpu = df_pod.groupby(["workload_type", "date"])["cpu_util"].mean().unstack().fillna(0)
    plt.figure(figsize=(10, 5))
    sns.heatmap(df_heat_cpu, annot=True, fmt=".1f", cmap="RdYlGn", cbar=True)
    plt.title("전사 평균 CPU 활용률 지형 히트맵 (%) (워크로드 유형 x 관측 일자)")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("워크로드 분류")
    plt.tight_layout()
    out4 = PLOT_DIR / "chart4_cpu_efficiency_heatmap.png"
    plt.savefig(out4, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out4.name}")

    print("⏳ [5/19] chart18_mem_waste_heatmap 격자 분석 중...")
    df_heat_mem = df_pod.groupby(["workload_type", "date"])["mem_waste_gb_hours"].sum().unstack().fillna(0)
    plt.figure(figsize=(10, 5))
    sns.heatmap(df_heat_mem, annot=False, cmap="BuPu", cbar=True)
    plt.title("전사 메모리 낭비 손실 총량 지형 히트맵 (GB-Hours)")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("워크로드 분류")
    plt.tight_layout()
    out18 = PLOT_DIR / "chart18_mem_waste_heatmap.png"
    plt.savefig(out18, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out18.name}")

    # ─── 📊 [차트 5 & 11] 거버넌스 파레토 누적 차트 ───
    print("⏳ [6/19] chart5_pareto_ns_waste 비용 거점 추적 중...")
    if not check_empty_data(df_ns, "chart5_pareto_ns_waste"):
        df_ns_top = df_ns.head(15)
        fig, ax1 = plt.subplots(figsize=(11, 5))
        sns.barplot(data=df_ns_top, x="namespace", y="total_waste_core_hours", ax=ax1, color="semibold")
        ax1.set_xticklabels(ax1.get_xticklabels(), rotation=45, ha="right")
        ax2 = ax1.twinx()
        ax2.plot(df_ns_top["namespace"], df_ns_top["waste_cumsum_pct"], color="crimson", marker="o", linewidth=2)
        ax2.set_ylim(0, 110)
        plt.title("전사 네임스페이스 상위 15개 비용 손실 파레토 거점 분포 (누적 지분율)")
        ax1.set_xlabel("부서/프로젝트 네임스페이스")
        ax1.set_ylabel("낭비 총량 (Core-Hours)")
        plt.tight_layout()
        out5 = PLOT_DIR / "chart5_pareto_ns_waste.png"
        plt.savefig(out5, dpi=100)
        plt.close()
        print(f"  -> 🎨 차트 렌더링 완료: {out5.name}")

    print("⏳ [7/19] chart11_pareto_workload_waste 기술 도메인 서열화 중...")
    df_wl_waste = df_pod.groupby("workload_type")["cpu_waste_core_hours"].sum().reset_index().sort_values("cpu_waste_core_hours", ascending=False)
    plt.figure(figsize=(10, 5))
    sns.barplot(data=df_wl_waste, x="workload_type", y="cpu_waste_core_hours", palette="Oranges_r")
    plt.xticks(rotation=30, ha="right")
    plt.title("워크로드 도메인 분류별 누적 CPU 낭비 총량 서열화")
    plt.xlabel("워크로드 분류")
    plt.ylabel("총 낭비량 (Core-Hours)")
    plt.tight_layout()
    out11 = PLOT_DIR / "chart11_pareto_workload_waste.png"
    plt.savefig(out11, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out11.name}")

    # ─── 📊 [차트 6] 자원 할당 상태 스코어 도넛 차트 ───
    print("⏳ [8/19] chart6_status_donut 자원 건전성 지표 요약 중...")
    status_summary = df_pod["status"].value_counts()
    plt.figure(figsize=(6, 5))
    colors = ["#70AD47", "#1F4E79", "#FFC000", "#C00000"]
    plt.pie(status_summary, labels=status_summary.index, autopct='%1.1f%%', startangle=90, colors=colors[:len(status_summary)], wedgeprops=dict(width=0.4, edgecolor='w'))
    plt.title("클러스터 인프라 자원 거버넌스 건전성 상태 등급 비율")
    plt.tight_layout()
    out6 = PLOT_DIR / "chart6_status_donut.png"
    plt.savefig(out6, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out6.name}")

    # ─── 📊 [차트 7 & 14] 낭비 입체 분포 분산도 차트 (Bubble & Scatter) ───
    print("⏳ [9/19] chart7_waste_footprint_bubble 3차원 입체 버블 맵 매핑 중...")
    df_bubble = df_pod.groupby("workload_type").agg(
        x_alloc=("cpu_allocated_core_hours", "sum"),
        y_util=("cpu_util", "mean"),
        z_waste=("cpu_waste_core_hours", "sum")
    ).reset_index()
    plt.figure(figsize=(9, 6))
    sns.scatterplot(data=df_bubble, x="x_alloc", y="y_util", size="z_waste", hue="workload_type", sizes=(100, 2000), alpha=0.7, legend="brief")
    plt.title("자원 풋프린트 입체 버블 분산도 (공급량 x 평균활용률 x 낭비규모 버블크기)")
    plt.xlabel("인프라 총 공급량 (Allocated Core-Hours)")
    plt.ylabel("평균 실효 활용률 (%)")
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    plt.tight_layout()
    out7 = PLOT_DIR / "chart7_waste_footprint_bubble.png"
    plt.savefig(out7, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out7.name}")

    print("⏳ [10/19] chart14_cpu_mem_waste_scatter 자원 교차 손실 스캔 중...")
    plt.figure(figsize=(8, 6))
    sns.scatterplot(data=df_pod.head(5000), x="cpu_waste_core_hours", y="mem_waste_gb_hours", hue="workload_type", alpha=0.5)
    plt.title("교차 손실 분산도: CPU 낭비량 vs Memory 낭비량 상관관계 분석 (샘플링)")
    plt.xlabel("CPU 낭비 (Core-Hours)")
    plt.ylabel("메모리 낭비 (GB-Hours)")
    plt.tight_layout()
    out14 = PLOT_DIR / "chart14_cpu_mem_waste_scatter.png"
    plt.savefig(out14, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out14.name}")

    # ─── 📊 [차트 8] 가용성 저하 리스크 분석 ───
    print("⏳ [11/19] chart8_shortfall_footprint 리소스 고갈 병목 분석 중...")
    df_short = df_pod.groupby(["workload_type", "date"])["cpu_shortage_cores"].sum().unstack().fillna(0)
    plt.figure(figsize=(10, 4))
    sns.heatmap(df_short, annot=False, cmap="YlOrRd", cbar=True)
    plt.title("클러스터 위험 팟 계통별 CPU 인프라 자원 부족(Deficit) 총량 지형")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("워크로드 분류")
    plt.tight_layout()
    out8 = PLOT_DIR / "chart8_shortfall_footprint.png"
    plt.savefig(out8, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out8.name}")

    # ─── 📊 [차트 9 & 10] 효율 통계 분위 분석 (Boxplot) ───
    print("⏳ [12/19] chart9_boxplot_cpu_util_by_workload 하중 변동성 밀도 유추 중...")
    plt.figure(figsize=(11, 5))
    sns.boxplot(data=df_pod, x="workload_type", y="cpu_util", palette="Set3")
    plt.xticks(rotation=30, ha="right")
    plt.ylim(-5, 105)
    plt.title("워크로드 유형별 P95 Peak CPU 활용률 분위 분포 박스플롯")
    plt.xlabel("워크로드 분류")
    plt.ylabel("P95 실제 활용률 (%)")
    plt.tight_layout()
    out9 = PLOT_DIR / "chart9_boxplot_cpu_util_by_workload.png"
    plt.savefig(out9, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out9.name}")

    print("⏳ [13/19] chart10_boxplot_mem_util_by_workload 하중 변동성 밀도 유추 중...")
    plt.figure(figsize=(11, 5))
    sns.boxplot(data=df_pod, x="workload_type", y="mem_util", palette="Pastel1")
    plt.xticks(rotation=30, ha="right")
    plt.ylim(-5, 105)
    plt.title("워크로드 유형별 P95 Peak 메모리 활용률 분위 분포 박스플롯")
    plt.xlabel("워크로드 분류")
    plt.ylabel("P95 실제 활용률 (%)")
    plt.tight_layout()
    out10 = PLOT_DIR / "chart10_boxplot_mem_util_by_workload.png"
    plt.savefig(out10, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out10.name}")

    # ─── 📊 [차트 12 & 15] 연쇄 결산 추이 및 장애 상태 격리 차트 ───
    print("⏳ [14/19] chart12_daily_waste_trend_by_workload 꺾은선 추이 추적 중...")
    df_trend_wl = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0)
    plt.figure(figsize=(11, 5))
    sns.lineplot(data=df_trend_wl, markers=True, dashes=False, linewidth=2)
    plt.title("기술 도메인 유형별 일별 CPU 자원 낭비량 시계열 변동 트렌드")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("낭비 손실량 (Core-Hours)")
    plt.xticks(rotation=30)
    plt.tight_layout()
    out12 = PLOT_DIR / "chart12_daily_waste_trend_by_workload.png"
    plt.savefig(out12, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out12.name}")

    print("⏳ [15/19] chart15_oom_status_by_workload 워크로드 자원 상태 분석 중...")
    plt.figure(figsize=(11, 5))
    sns.countplot(data=df_pod, x="workload_type", hue="status", palette="muted")
    plt.xticks(rotation=30, ha="right")
    plt.title("워크로드 분류체계별 자원 할당 거버넌스 상태 분포 빈도 수 카운트")
    plt.xlabel("워크로드 분류")
    plt.ylabel("팟 유닛 수량 (개)")
    plt.legend(loc="upper right")
    plt.tight_layout()
    out15 = PLOT_DIR / "chart15_oom_status_by_workload.png"
    plt.savefig(out15, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out15.name}")

    # ─── 📊 [차트 13 & 17] 자원 정규 분포 및 거버넌스 마진률 바이올린 플롯 ───
    print("⏳ [16/19] chart13_violin_cpu_util 고밀도 커널 파동 연산 중...")
    plt.figure(figsize=(10, 5))
    sns.violinplot(data=df_pod, x="workload_type", y="cpu_util", inner="quartile", palette="pastel")
    plt.xticks(rotation=30, ha="right")
    plt.title("인프라 활용률 커널 밀도 추정(KDE) 바이올린 분포 곡선")
    plt.xlabel("워크로드 분류")
    plt.ylabel("CPU 실제 활용률 (%)")
    plt.tight_layout()
    out13 = PLOT_DIR / "chart13_violin_cpu_util.png"
    plt.savefig(out13, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out13.name}")

    print("⏳ [17/19] chart17_cpu_limit_request_ratio QoS 안정성 등급 비율 측정 중...")
    plt.figure(figsize=(10, 5))
    sns.boxplot(data=df_pod, x="workload_type", y="lim_req_ratio", palette="vlag")
    plt.xticks(rotation=30, ha="right")
    plt.title("쿠버네티스 팟 유형별 CPU Limit / Request 설정 오버커밋(Overcommit) 배율 분포")
    plt.xlabel("워크로드 분류")
    plt.ylabel("Limit / Request 비율 (배율)")
    plt.tight_layout()
    out17 = PLOT_DIR / "chart17_cpu_limit_request_ratio.png"
    plt.savefig(out17, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out17.name}")

    # ─── 📊 [차트 19 & 20] 일별 워크로드 상세 시계열 면적 차트 ───
    print("⏳ [18/19] chart19_daily_cpu_per_workload 일간 CPU 총괄 볼륨 스캔 중...")
    df_daily_cpu_req = df_pod.groupby("date")["cpu_request_max"].sum()
    df_daily_cpu_use = df_pod.groupby("date")["cpu_usage_p95"].sum()
    plt.figure(figsize=(11, 5))
    plt.fill_between(df_daily_cpu_req.index, df_daily_cpu_req.values, label="전사 총 CPU 공급 공급량 (Request Cores)", color="skyblue", alpha=0.4)
    plt.plot(df_daily_cpu_use.index, df_daily_cpu_use.values, label="전사 총 CPU 피크 실효 소모량 (P95 Cores)", color="navy", linewidth=2.5, marker="o")
    plt.title("클러스터 일별 총 CPU 인프라 공급 사양 vs 실제 피크 연산 소모량 추이 (KST)")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("CPU 총 합산 코어 수 (Cores)")
    plt.xticks(rotation=30)
    plt.legend(loc="upper left")
    plt.tight_layout()
    out19 = PLOT_DIR / "chart19_daily_cpu_per_workload.png"
    plt.savefig(out19, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out19.name}")

    print("⏳ [19/19] chart20_daily_mem_per_workload 일간 메모리 총괄 볼륨 스캔 중...")
    df_daily_mem_req = df_pod.groupby("date")["mem_request_max"].sum()
    df_daily_mem_use = df_pod.groupby("date")["mem_usage_p95"].sum()
    plt.figure(figsize=(11, 5))
    plt.fill_between(df_daily_mem_req.index, df_daily_mem_req.values, label="전사 총 Memory 공급 공급량 (Request GB)", color="plum", alpha=0.4)
    plt.plot(df_daily_mem_use.index, df_daily_mem_use.values, label="전사 총 Memory 피크 실효 소모량 (P95 GB)", color="purple", linewidth=2.5, marker="o")
    plt.title("클러스터 일별 총 Memory 인프라 공급 사양 vs 실제 피크 소모량 추이 (KST)")
    plt.xlabel("관측 일자 (KST)")
    plt.ylabel("메모리 총 합산 용량 (GB)")
    plt.xticks(rotation=30)
    plt.legend(loc="upper left")
    plt.tight_layout()
    out20 = PLOT_DIR / "chart20_daily_mem_per_workload.png"
    plt.savefig(out20, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 렌더링 완료: {out20.name}")

    print("\n🏁 === [한글 보정판 Step3 차트 드로잉 성공 마감] 모든 한글 이미지 자산이 ./data/output/plots/ 에 동기화되었습니다. ===")

if __name__ == "__main__":
    main()

0개의 댓글