"""
[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)
def draw_node_distribution_chart(df_node, out_dir):
if df_node.empty:
print("⚠️ [안내] 노드 집계 데이터가 비어있어 분포도 차트를 그리지 않습니다.")
return
fig, ax = plt.subplots(figsize=(12, 6))
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()
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)
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()