26J29e3

Young-Kyoo Kim·2026년 6월 28일
"""
[3단계-노드 패치본] Range=0 에러 방어 가드가 포함된 노드 효율성 분석 및 마스터 차트 빌드
실행: python step3_node_efficiency.py
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path

from config import MERGED_DIR, OUT_DIR

def init_plot_style():
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = ['Arial', 'Helvetica', 'DejaVu Sans', 'Liberation Sans']
    plt.rcParams['axes.unicode_minus'] = False 
    sns.set_theme(style="whitegrid")
    plt.rcParams['figure.dpi'] = 150
    plt.rcParams['axes.titlesize'] = 12
    plt.rcParams['axes.labelsize'] = 10

def analyze_node_level_efficiency():
    src_file = MERGED_DIR / "enriched_fixed_7d.parquet"
    if not src_file.exists():
        print(f"❌ [에러] {src_file.name} 파일이 없습니다. step2_5 보정 스크립트를 먼저 실행해 주세요.")
        return

    df = pd.read_parquet(src_file)
    
    if "mem_allocated_gb_hours" not in df.columns:
        df["mem_allocated_gb_hours"] = df["mem_request_max"].astype(float) * (df["minutes_running"].astype(float) / 60.0)
        df["mem_usage_gb_hours"] = df["mem_usage_p95"].astype(float) * (df["minutes_running"].astype(float) / 60.0)
    
    print(f"📊 노드 레벨 자원 프로파일링 기동... (관측 데이터 행수: {len(df):,}개)")

    total_cpu_alloc_hrs = df["cpu_allocated_core_hours"].sum()
    total_cpu_use_hrs = df["cpu_usage_core_hours"].sum()
    total_mem_alloc_hrs = df["mem_allocated_gb_hours"].sum()
    total_mem_use_hrs = df["mem_usage_gb_hours"].sum()

    global_cpu_eff = (total_cpu_use_hrs / max(0.1, total_cpu_alloc_hrs)) * 100
    global_mem_eff = (total_mem_use_hrs / max(0.1, total_mem_alloc_hrs)) * 100

    df_node = df.groupby("node").agg(
        running_pods_cnt    = ("container", "count"),
        node_cpu_alloc_hrs  = ("cpu_allocated_core_hours", "sum"),
        node_cpu_use_hrs    = ("cpu_usage_core_hours", "sum"),
        node_mem_alloc_hrs  = ("mem_allocated_gb_hours", "sum"),
        node_mem_use_hrs    = ("mem_usage_gb_hours", "sum")
    ).reset_index()

    df_node["cpu_efficiency_pct"] = (df_node["node_cpu_use_hrs"] / df_node["node_cpu_alloc_hrs"].clip(lower=0.1) * 100).round(2)
    df_node["mem_efficiency_pct"] = (df_node["node_mem_use_hrs"] / df_node["node_mem_alloc_hrs"].clip(lower=0.1) * 100).round(2)
    
    df_node["cpu_waste_core_hours"] = (df_node["node_cpu_alloc_hrs"] - df_node["node_cpu_use_hrs"]).clip(lower=0)
    df_node["mem_waste_gb_hours"] = (df_node["node_mem_alloc_hrs"] - df_node["node_mem_use_hrs"]).clip(lower=0)

    df_node.to_parquet(MERGED_DIR / "agg_node_efficiency.parquet", index=False)
    
    plots_dir = OUT_DIR / "plots"
    plots_dir.mkdir(parents=True, exist_ok=True)

    draw_global_summary_chart(global_cpu_eff, global_mem_eff, plots_dir)
    draw_node_distribution_chart(df_node, plots_dir)
    draw_top_waste_nodes_chart(df_node, plots_dir)

def draw_global_summary_chart(cpu_eff, mem_eff, out_dir):
    fig, ax = plt.subplots(figsize=(7, 5))
    metrics = ['Global CPU Efficiency', 'Global Memory Efficiency']
    values = [cpu_eff, mem_eff]
    
    bars = ax.bar(metrics, values, color=['#1F4E79', '#2E75B6'], width=0.4, edgecolor='gray', linewidth=0.5)
    ax.set_title('Global Infrastructure Efficiency Summary', pad=20, weight='bold')
    ax.set_ylabel('Resource Efficiency Score (%)', labelpad=12)
    ax.set_ylim(0, 110)
    ax.axhline(y=30, color='red', linestyle=':', label='Target Min Guideline (30%)')
    
    for bar in bars:
        height = bar.get_height()
        ax.annotate(f"{height:.2f}%", (bar.get_x() + bar.get_width()/2., height),
                    ha='center', va='bottom', fontsize=10, weight='bold', xytext=(0, 5), textcoords='offset points')
    ax.legend(loc='upper right')
    plt.tight_layout()
    fig.savefig(out_dir / "chart_n1_global_summary.png")
    plt.close(fig)

# ── [🛡️ 패치 반영] 데이터 단일화로 인한 bins=0 에러 방어 히스트플롯 ──
def draw_node_distribution_chart(df_node, out_dir):
    if df_node.empty:
        print("⚠️ [안내] 노드 집계 데이터가 비어있어 분포도 차트를 그리지 않습니다.")
        return

    fig, ax = plt.subplots(figsize=(12, 6))
    
    # 💡 핵심 방어: 데이터 범위(Range) 추출
    cpu_range = df_node['cpu_efficiency_pct'].max() - df_node['cpu_efficiency_pct'].min()
    mem_range = df_node['mem_efficiency_pct'].max() - df_node['mem_efficiency_pct'].min()
    
    # CPU 분포도 그리기 (모든 노드 점수가 같으면 binwidth=5를 해제하고 고정 bins=5 적용)
    if cpu_range == 0 or np.isnan(cpu_range):
        sns.histplot(data=df_node, x='cpu_efficiency_pct', bins=5, kde=False, color='#1F4E79', ax=ax, label='CPU Node Density (Constant)', alpha=0.6)
    else:
        sns.histplot(data=df_node, x='cpu_efficiency_pct', binwidth=5, kde=True, color='#1F4E79', ax=ax, label='CPU Node Density', alpha=0.6)
        
    # Memory 분포도 그리기 (에러 방어 동시 적용)
    if mem_range == 0 or np.isnan(mem_range):
        sns.histplot(data=df_node, x='mem_efficiency_pct', bins=5, kde=False, color='#E67E22', ax=ax, label='Memory Node Density (Constant)', alpha=0.4)
    else:
        sns.histplot(data=df_node, x='mem_efficiency_pct', binwidth=5, kde=True, color='#E67E22', ax=ax, label='Memory Node Density', alpha=0.4)
    
    ax.set_title('Multi-Cluster Per-Node Efficiency Density Distribution Profiling', pad=20, weight='bold')
    ax.set_xlabel('Resource Efficiency Scores (%)', labelpad=12, weight='bold')
    ax.set_ylabel('Node Count (Frequency)', labelpad=12)
    ax.set_xlim(-5, 105)
    
    ax.axvline(x=15, color='red', linestyle='--', linewidth=1.2, label='Severe Underuse Line (15%)')
    ax.axvline(x=50, color='green', linestyle='--', linewidth=1.2, label='Highly Optimized Line (50%)')
    
    ax.legend(loc='upper right', frameon=True)
    plt.tight_layout()
    fig.savefig(out_dir / "chart_n2_node_distribution.png")
    plt.close(fig)

def draw_top_waste_nodes_chart(df_node, out_dir):
    df_top = df_node.sort_values(by="cpu_waste_core_hours", ascending=False).head(20).reset_index(drop=True)
    if df_top.empty or (df_top["cpu_waste_core_hours"] == 0).all():
        print("⚠️ [안내] 낭비 리소스 코어가 전 노드 0이므로 상위 낭비 노드 저격 차트를 스킵합니다.")
        return
        
    fig, ax = plt.subplots(figsize=(14, 7))
    df_top['node_short'] = df_top['node'].apply(lambda x: x.split('.')[-1] if '.' in x else x)
    df_top['node_short'] = df_top['node_short'].str.replace('icdlhk8s', '')
    
    sns.barplot(data=df_top, x='node_short', y='cpu_waste_core_hours', palette='Reds_r', ax=ax, hue='node_short', legend=False)
    
    ax.set_title('Top 20 Nodes by Absolute Resource Waste Size', pad=20, weight='bold')
    ax.set_xlabel('Target Node Identifiers', labelpad=12, weight='bold')
    ax.set_ylabel('Total Waste Scale (CPU Core-Hours)', labelpad=12)
    
    ax.set_ylim(0, max(10, df_top['cpu_waste_core_hours'].max() * 1.20))
    ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
    
    for idx, p in enumerate(ax.patches):
        val = p.get_height()
        if idx < len(df_top):
            eff_score = df_top['cpu_efficiency_pct'].iloc[idx]
            pod_cnt = df_top['running_pods_cnt'].iloc[idx]
            if val >= 0:
                ax.annotate(f"Eff: {eff_score:.1f}%\n({int(pod_cnt)} Pods)", 
                            (p.get_x() + p.get_width() / 2., val), 
                            ha='center', va='bottom', fontsize=8, color='#333333', 
                            xytext=(0, 5), textcoords='offset points', weight='bold')
            
    plt.tight_layout()
    fig.savefig(out_dir / "chart_n3_top_waste_nodes.png")
    plt.close(fig)
    print("✅ [패치 완수] 노드 효율성 분석 마스터 이미지 3종이 안전하게 저장되었습니다.")

if __name__ == "__main__":
    init_plot_style()
    analyze_node_level_efficiency()

0개의 댓글