"""
[3-2단계-추이] 일자별(Daily) 자원 효율성 추이 분석 및 트렌드 마스터 차트 2종 빌드
실행: python step3_2_daily_trends.py
입력: data/merged/enriched_fixed_7d.parquet (2.5단계 시간 보정본)
출력: data/merged/agg_daily_trends.parquet (일자별 통합 통계 원부)
data/output/plots/chart_t1_daily_efficiency_trend.png (일자별 CPU/MEM 효율성 선 그래프)
data/output/plots/chart_t2_daily_waste_stack.png (일자별 워크로드별 낭비량 누적 막대 그래프)
"""
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
plt.rcParams['xtick.labelsize'] = 9
plt.rcParams['ytick.labelsize'] = 9
def analyze_daily_trends():
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)
df['date'] = df['date'].astype(str)
df['workload_type'] = df['workload_type'].str.upper()
print(f"📈 일자별 타임시리즈 FinOps 프로파일링 기동... (관측 데이터 행수: {len(df):,}개)")
df_daily = df.groupby("date").agg(
total_cpu_alloc_hrs = ("cpu_allocated_core_hours", "sum"),
total_cpu_use_hrs = ("cpu_usage_core_hours", "sum"),
total_mem_alloc_hrs = ("mem_allocated_gb_hours", "sum"),
total_mem_use_hrs = ("mem_usage_gb_hours", "sum"),
total_cpu_waste_hrs = ("cpu_waste_core_hours", "sum")
).reset_index().sort_values("date")
df_daily["cpu_efficiency_pct"] = (df_daily["total_cpu_use_hrs"] / df_daily["total_cpu_alloc_hrs"].clip(lower=0.1) * 100).round(2)
df_daily["mem_efficiency_pct"] = (df_daily["total_mem_use_hrs"] / df_daily["total_mem_alloc_hrs"].clip(lower=0.1) * 100).round(2)
df_daily.to_parquet(MERGED_DIR / "agg_daily_trends.parquet", index=False)
df_daily_wl = df.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0)
df_daily_wl = df_daily_wl.sort_index()
plots_dir = OUT_DIR / "plots"
plots_dir.mkdir(parents=True, exist_ok=True)
draw_daily_efficiency_trend(df_daily, plots_dir)
draw_daily_waste_stack(df_daily_wl, plots_dir)
print("\n" + "="*70)
print("📢 [Daily Trend Summary] 일자별 클러스터 자원 효율성 변동 추이")
print("="*70)
for _, row in df_daily.iterrows():
print(f" • 날짜: {row['date']} | CPU 효율: {row['cpu_efficiency_pct']}% | MEM 효율: {row['mem_efficiency_pct']}% | 낭비량: {row['total_cpu_waste_hrs']:,.1f} Core-Hours")
print("="*70)
def draw_daily_efficiency_trend(df_daily, out_dir):
fig, ax = plt.subplots(figsize=(12, 5.5))
ax.plot(df_daily['date'], df_daily['cpu_efficiency_pct'], marker='o', linewidth=2.5, color='#1F4E79', label='Global CPU Efficiency (%)')
ax.plot(df_daily['date'], df_daily['mem_efficiency_pct'], marker='s', linewidth=2.5, color='#E67E22', label='Global Memory Efficiency (%)')
ax.set_title('Global Infrastructure Resource Efficiency Daily Trend', pad=20, weight='bold')
ax.set_xlabel('Observation Dates', labelpad=12, weight='bold')
ax.set_ylabel('P95 Efficiency Score (%)', labelpad=12)
ax.set_ylim(0, 110)
ax.axhline(y=30, color='red', linestyle=':', linewidth=1.2, label='Target Efficiency Bottom Line (30%)')
for i, row in df_daily.iterrows():
ax.annotate(f"{row['cpu_efficiency_pct']:.1f}%", (row['date'], row['cpu_efficiency_pct']), textcoords="offset points", xytext=(0,10), ha='center', fontsize=8, color='#1F4E79', weight='bold')
ax.annotate(f"{row['mem_efficiency_pct']:.1f}%", (row['date'], row['mem_efficiency_pct']), textcoords="offset points", xytext=(0,-15), ha='center', fontsize=8, color='#E67E22', weight='bold')
ax.legend(loc='upper right', frameon=True)
plt.tight_layout()
fig.savefig(out_dir / "chart_t1_daily_efficiency_trend.png")
plt.close(fig)
def draw_daily_waste_stack(df_daily_wl, out_dir):
"""어떤 기술 스택이 특정 날짜의 비용 스파이크를 유발했는지 추적하는 누적 바 차트"""
fig, ax = plt.subplots(figsize=(13, 6))
df_daily_wl.plot(kind='bar', stacked=True, ax=ax, cmap='Set3', edgecolor='gray', linewidth=0.4, width=0.55)
ax.set_title('Daily CPU Waste Footprint: Broken Down by Workload Technical Domains', pad=20, weight='bold')
ax.set_xlabel('Observation Dates', labelpad=12, weight='bold')
ax.set_ylabel('Total Waste Scale (Core-Hours)', labelpad=12)
ax.set_xticklabels(ax.get_xticklabels(), rotation=0, ha='center')
max_val = df_daily_wl.sum(axis=1).max()
ax.set_ylim(0, max_val * 1.20)
ax.get_yaxis().set_major_formatter(plt.FuncFormatter(lambda x, loc: "{:,}".format(int(x))))
for i, (idx, row) in enumerate(df_daily_wl.iterrows()):
total_waste = row.sum()
if total_waste > 0:
ax.text(i, total_waste + (max_val * 0.02), f"{total_waste:,.1f} CHrs", ha='center', va='bottom', fontsize=8, weight='bold', color='#333333')
ax.legend(title='Workload Domains', bbox_to_anchor=(1.02, 1), loc='upper left', fontsize=9, frameon=True)
plt.tight_layout()
fig.savefig(out_dir / "chart_t2_daily_waste_stack.png", bbox_inches='tight')
plt.close(fig)
print("✅ [Trend Charts] Both time-series trend charts saved successfully.")
if __name__ == "__main__":
print("=== [3-2단계-추이] 인프라 자원 효율성 일자별 시계열 전수조사 엔진 가동 ===")
init_plot_style()
analyze_daily_trends()