26Y01a4

Young-Kyoo KimΒ·μ•½ 16μ‹œκ°„ μ „
"""
step3_analytics.py β€” Advanced Infrastructure Resource Visualization Engine (English Standard)
"""
import os
import re
import pandas as pd
import numpy as np
from pathlib import Path

# ─── πŸ›‘οΈ [Headless Environment Guard] Prevents crashes in K8s pods without display servers ───
import matplotlib
matplotlib.use('Agg') 
import matplotlib.pyplot as plt
import seaborn as sns

# ─── πŸ“‚ [경둜 동기화] step6와 μ™„λ²½νžˆ μΌμΉ˜ν•˜λŠ” ./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)

# ─── πŸ”€ [No More Tofu] Use bulletproof built-in standard fonts ───
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False  # Prevents minus sign (-) corruption
sns.set_theme(style="whitegrid")

def check_empty_data(df, chart_name):
    if df.empty:
        print(f"  ⚠️  [Empty Data] Skipping {chart_name} due to empty rows.")
        return True
    return False

def main():
    print("πŸš€ [Step3 κ°œμ‹œ] FinOps 닀차원 μ‹œκ³„μ—΄ 영문 μ‹œκ°ν™” 뢄석 μ—”μ§„ 가동...")
    
    p1 = MERGED_DIR / "enriched_fixed_7d.parquet"
    p2 = MERGED_DIR / "pareto_fixed_ns.parquet"
    
    print(f"πŸ“– 가곡 원뢀 μŠ€μΊ” 경둜:\n   - {p1}\n   - {p2}")
    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):,}ν–‰ μŠ€μΊ” 성곡.")

    # ─── πŸ“Š [μ§€ν‘œ μ—°μ‚° 및 영문 λ§€ν•‘ λ ˆμ΄μ–΄] ─────────────────────────────────
    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)
    
    # 차트 λ²”λ‘€ κΈ€μž 깨짐 λ°©μ§€λ₯Ό μœ„ν•œ ν•œκΈ€ μƒνƒœκ°’ ➑️ 영문 λ‹€μ΄λ ‰νŠΈ 컨버전
    df_pod["status_en"] = df_pod["status"].map({
        "πŸ’₯ OOMμž₯μ• λ°œμƒ": "OOM Killed", 
        "⚠️ RequestλΆ€μ‘±": "Request Shortage", 
        "πŸ“‰ κ³Όλ‹€ν• λ‹Ή": "Over-allocated", 
        "βœ… μ΅œμ ν™”μ™„λ£Œ": "Optimized"
    }).fillna("Unknown")

    # ─── πŸ“Š [차트 1 & 2] μ›Œν¬λ‘œλ“œ νƒ€μž…λ³„ μžμ› ν• λ‹Ή vs μ‹€μ‚¬μš© 피크 뢄석 ───
    print("\n⏳ [1/18] 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("Average CPU Request vs P95 Peak Usage by Workload Type")
    plt.xlabel("Workload Type")
    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/18] chart2_mem_req_vs_usage_by_workload 고도화(Memory RSS λ ˆμ΄μ–΄ νŽΈμž…) μ—°μ‚° 쀑...")
    # πŸ’‘ [고도화 μΈμ‚¬μ΄νŠΈ] Working Setκ³Ό 순수 RSS λ©”λͺ¨λ¦¬ 격차λ₯Ό ν•œλˆˆμ— λ³Ό 수 μžˆλ„λ‘ 3쀑 λ°” 차트둜 ν™•μž₯
    df_wl_mem = df_pod.groupby("workload_type")[["mem_request_max", "mem_usage_p95", "mem_rss_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", "mem_rss_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("Average Memory Specification vs WorkingSet vs RSS Peak (GB)")
    plt.xlabel("Workload Type")
    plt.ylabel("Memory Capacity (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/18] 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("Daily Total CPU Waste Core-Hours Stacked by Workload Type (KST)")
        plt.xlabel("Date (KST)")
        plt.ylabel("Waste Volume (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/18] 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("Mean CPU Utilization Heatmap (%) (Workload Type x Date)")
    plt.xlabel("Date (KST)")
    plt.ylabel("Workload Type")
    plt.tight_layout()
    out4 = PLOT_DIR / "chart4_cpu_efficiency_heatmap.png"
    plt.savefig(out4, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 λ Œλ”λ§ μ™„λ£Œ: {out4.name}")

    print("⏳ [5/18] 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("Total Memory Waste Volume Heatmap (GB-Hours)")
    plt.xlabel("Date (KST)")
    plt.ylabel("Workload Type")
    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/18] 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="steelblue")
        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("Top 15 Namespace Cost-Waste Pareto Chart (Cumulative Share %)")
        ax1.set_xlabel("Tenant Namespace")
        ax1.set_ylabel("Waste Volume (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/18] 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("Total CPU Waste Volume by Workload Type")
    plt.xlabel("Workload Type")
    plt.ylabel("Total Waste (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/18] chart6_status_donut κ±°λ²„λ„ŒμŠ€ 건전성 λΉ„μœ¨ λ„μΆœ 쀑...")
    status_summary = df_pod["status_en"].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("Infrastructure Resource Governance Status Ratio")
    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/18] 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("Resource Footprint Bubble Chart (Allocated x Utilization x Waste Size)")
    plt.xlabel("Total Allocated Core-Hours")
    plt.ylabel("Average Utilization (%)")
    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/18] chart14_cpu_mem_waste_scatter μžμ› 손싀 ꡐ차 ꡐ정 νŠΈλž˜ν‚Ή 쀑...")
    # πŸ’‘ [고도화 μΈμ‚¬μ΄νŠΈ] PV μŠ€ν† λ¦¬μ§€ λ‚­λΉ„λŸ‰(pv_waste_gb_hours)을 점의 크기(Size)둜 μ—°λ™ν•˜μ—¬ 인프라 μž…μ²΄ 진단 μˆ˜ν–‰
    plt.figure(figsize=(9, 6))
    sns.scatterplot(data=df_pod.head(5000), x="cpu_waste_core_hours", y="mem_waste_gb_hours", size="pv_waste_gb_hours", hue="workload_type", alpha=0.5, sizes=(20, 400))
    plt.title("Multi-Resource Loss Co-relation: CPU vs Memory vs PV Storage (Size)")
    plt.xlabel("CPU Waste (Core-Hours)")
    plt.ylabel("Memory Waste (GB-Hours)")
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
    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/18] chart8_shortfall_footprint λ¦¬μ†ŒμŠ€ λΆ€μ‘± 및 μŠ€λ‘œν‹€λ§ 병λͺ© 탐지 쀑...")
    # πŸ’‘ [고도화 μΈμ‚¬μ΄νŠΈ] λ‹¨μˆœ λΆ€μ‘± 수치 외에, μƒˆλ‘­κ²Œ μˆ˜μ§‘λœ CPU CFS μŠ€λ‘œν‹€λ§ μ§€ν‘œλ₯Ό 히트맡 μ§€ν˜•μœΌλ‘œ ꡬ성
    df_short = df_pod.groupby(["workload_type", "date"])["cpu_throttled_max"].max().unstack().fillna(0)
    plt.figure(figsize=(10, 4))
    sns.heatmap(df_short, annot=False, cmap="YlOrRd", cbar=True)
    plt.title("Container CPU CFS Throttled Periods Peak Footprint (Risk Matrix)")
    plt.xlabel("Date (KST)")
    plt.ylabel("Workload Type")
    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/18] chart9_boxplot_cpu_util_by_workload CPU ν•˜μ€‘ 뢄포도 μ—°μ‚° 쀑...")
    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("CPU Utilization P95 Boxplot Distribution by Workload Type")
    plt.xlabel("Workload Type")
    plt.ylabel("P95 Actual Utilization (%)")
    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/18] 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("Memory Utilization P95 Boxplot Distribution by Workload Type")
    plt.xlabel("Workload Type")
    plt.ylabel("P95 Actual Utilization (%)")
    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/18] 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("Daily CPU Waste Timeline Trend Line by Workload Type")
    plt.xlabel("Date (KST)")
    plt.ylabel("Waste Volume (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/18] chart15_oom_status_by_workload μž₯μ• κ΅° 카운트 λΆ„λ₯˜ 쀑...")
    plt.figure(figsize=(11, 5))
    sns.countplot(data=df_pod, x="workload_type", hue="status_en", palette="muted")
    plt.xticks(rotation=30, ha="right")
    plt.title("Governance Status Distribution Count per Workload Type")
    plt.xlabel("Workload Type")
    plt.ylabel("Pod Count")
    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/18] 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("CPU Utilization Kernel Density Violin Plot")
    plt.xlabel("Workload Type")
    plt.ylabel("CPU Utilization (%)")
    plt.tight_layout()
    out13 = PLOT_DIR / "chart13_violin_cpu_util.png"
    plt.savefig(out13, dpi=100)
    plt.close()
    print(f"  -> 🎨 차트 λ Œλ”λ§ μ™„λ£Œ: {out13.name}")

    print("⏳ [17/18] 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("Kubernetes Pod CPU Limit / Request Overcommit Ratio")
    plt.xlabel("Workload Type")
    plt.ylabel("Limit / Request Ratio (Factor)")
    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("⏳ [17/18] 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="Total CPU Request Cores (Capacity)", color="skyblue", alpha=0.4)
    plt.plot(df_daily_cpu_use.index, df_daily_cpu_use.values, label="Total CPU P95 Actual Consumption", color="navy", linewidth=2.5, marker="o")
    plt.title("Daily Total CPU Capacity Allocation vs Actual Peak Consumption Trend (KST)")
    plt.xlabel("Date (KST)")
    plt.ylabel("Total 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("⏳ [18/18] 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()
    # πŸ’‘ [고도화 μΈμ‚¬μ΄νŠΈ] λ©”λͺ¨λ¦¬μ™€ ν•¨κ»˜ 총 PV ν• λ‹Ή μš©λŸ‰μ˜ μ‹œκ³„μ—΄ μŠ€μΌ€μΌμ„ μ„œλΈŒ μ μ„ μœΌλ‘œ λ ˆμ΄μ•„μ›ƒ ꡬ성
    df_daily_pv_req  = df_pod.groupby("date")["pv_capacity_max"].sum()
    
    fig, ax1 = plt.subplots(figsize=(11, 5))
    ax1.fill_between(df_daily_mem_req.index, df_daily_mem_req.values, label="Total Memory Request (GB)", color="plum", alpha=0.4)
    ax1.plot(df_daily_mem_use.index, df_daily_mem_use.values, label="Total Memory P95 Actual Usage", color="purple", linewidth=2.5, marker="o")
    ax1.set_ylabel("Memory Volume (GB)")
    ax1.set_xlabel("Date (KST)")
    
    ax2 = ax1.twinx()
    ax2.plot(df_daily_pv_req.index, df_daily_pv_req.values, label="Total PV Provisions (GB)", color="teal", linewidth=2, linestyle="--", marker="x")
    ax2.set_ylabel("PV Storage Volume (GB)")
    
    plt.title("Daily Memory & PV Storage Space Provisions vs Active Peak Consumption")
    fig.legend(loc="upper left", bbox_to_anchor=(0.1, 0.9))
    plt.xticks(rotation=30)
    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 μ™„κ²°] κΈ€μž 깨짐이 μ›μ²œ λ°•λ©Έλœ 19λŒ€ λ§ˆμŠ€ν„° 영문 μ°¨νŠΈκ°€ ./data/output/plots/ 에 λ™κΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€. ===")

if __name__ == "__main__":
    main()

0개의 λŒ“κΈ€