"""
step3_analytics.py — 장기 시계열 인프라 데이터 시각화 및 20대 마스터 한글 차트 드로잉 엔진
"""
import os
import re
import pandas as pd
import numpy as np
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns
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)
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
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")
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)
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}")
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}")
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}")
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}")
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}")
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}")
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}")
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}")
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}")
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}")
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()