26J29d2

Young-Kyoo Kim·2026년 6월 28일
"""
[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

    # 1. 보정 데이터 로드
    df = pd.read_parquet(src_file)
    
    # 데이터 내에 date 컬럼이 문자열인 경우 정렬을 위해 변환 방어
    df['date'] = df['date'].astype(str)
    df['workload_type'] = df['workload_type'].str.upper()
    
    print(f"📈 일자별 타임시리즈 FinOps 프로파일링 기동... (관측 데이터 행수: {len(df):,}개)")

    # ── 📌 1. 일자별(Daily) 글로벌 자원 효율성 롤업 집계 ──
    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)

    # ── 📌 2. 일자별 + 워크로드 도메인별 다차원 집계 (스택 차트용) ──
    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)

    # 2대 트렌드 마스터 차트 드로잉
    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)

# ── [차트 T1] 일자별 CPU vs MEM 종합 자원 효율성 변동 트렌드 ──────────────────
def draw_daily_efficiency_trend(df_daily, out_dir):
    fig, ax = plt.subplots(figsize=(12, 5.5))
    
    # CPU 및 Memory 추이 선 그래프 드로잉 (글자 겹침 방지 여백 확보)
    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)
    
    # FinOps 거버넌스 하한선 마킹
    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)

# ── [차트 T2] 일자별 워크로드 도메인별 CPU 낭비 규모 추이 (누적 스택) ───────────
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')
    
    # Y축 최대값 버퍼 상단 강제 부여
    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()

0개의 댓글