Tôi là Minh, Lead AI Engineer tại một startup thương mại điện tử quy mô 200 người. Tháng 3 vừa rồi, đội ngũ product yêu cầu tích hợp AI vào hệ thống chăm sóc khách hàng tự động. Thử nghiệm ban đầu với GPT-4 cho kết quả tốt, nhưng khi so sánh chi phí với Claude và Gemini thì phát hiện: chênh lệch chi phí lên đến 6 lần cho cùng một tác vụ. Từ đó tôi xây dựng một hệ thống benchmark tự động để đánh giá chất lượng và chi phí trên tất cả các mô hình. Bài viết này chia sẻ toàn bộ cách tôi triển khai hệ thống đó với HolySheep AI.

Tại sao cần benchmark đa mô hình?

Trong thực tế triển khai, không phải lúc nào model đắt nhất cũng là lựa chọn tốt nhất. Tôi đã chứng kiến nhiều dự án chi trả $15/MTok cho Claude chỉ vì thương hiệu, trong khi Gemini 2.5 Flash cho kết quả tương đương với chi phí chỉ bằng 1/6. Hệ thống benchmark giúp bạn:

Kiến trúc hệ thống Benchmark

Hệ thống gồm 4 thành phần chính: Prompt Library → Parallel Runner → Scoring Engine → Report Generator. Toàn bộ chạy trên HolySheep AI với chi phí tiết kiệm 85%+ so với API gốc.

Cài đặt môi trường

# Cài đặt dependencies
pip install openai httpx pandas numpy tqdm scipy python-dotenv

Hoặc sử dụng poetry

poetry add openai httpx pandas numpy tqdm scipy python-dotenv

Tạo file .env

cat > .env << 'EOF' HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY HOLYSHEEP_BASE_URL=https://api.holysheep.ai/v1 EOF

Module 1: Prompt Library và Test Cases

Đầu tiên, tôi xây dựng một prompt library đa dạng bao gồm các tác vụ thực tế: customer support, code generation, summarization, translation, creative writing, và reasoning. Mỗi test case có ground truth để đánh giá tự động.

import json
import hashlib
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from enum import Enum

class TaskCategory(Enum):
    CUSTOMER_SUPPORT = "customer_support"
    CODE_GENERATION = "code_generation"
    SUMMARIZATION = "summarization"
    TRANSLATION = "translation"
    CREATIVE_WRITING = "creative_writing"
    REASONING = "reasoning"
    RAG_ANSWERING = "rag_answering"

@dataclass
class TestCase:
    id: str
    category: TaskCategory
    prompt: str
    context: Optional[str] = None
    ground_truth: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)
    
    def __post_init__(self):
        if not self.id:
            self.id = hashlib.md5(
                self.prompt.encode() + str(self.category.value).encode()
            ).hexdigest()[:12]

class PromptLibrary:
    """Thư viện prompt đa dạng cho benchmark"""
    
    def __init__(self):
        self.test_cases: List[TestCase] = []
        self._init_standard_cases()
    
    def _init_standard_cases(self):
        # Customer Support Cases
        self.test_cases.extend([
            TestCase(
                category=TaskCategory.CUSTOMER_SUPPORT,
                prompt="Khách hàng phàn nàn: 'Tôi đã đặt hàng 5 ngày rồi mà vẫn chưa nhận được. Đơn hàng #123456. Tôi rất không hài lòng!' Hãy viết phản hồi chuyên nghiệp, giải quyết vấn đề và giữ chân khách.",
                ground_truth="Cần có: (1) Xin lỗi, (2) Kiểm tra đơn hàng, (3) Đề xuất giải pháp, (4) Bồi thường phù hợp"
            ),
            TestCase(
                category=TaskCategory.CUSTOMER_SUPPORT,
                prompt="Giải thích chính sách đổi trả trong 30 ngày cho khách hàng mua sắm online. Bao gồm điều kiện, quy trình, và thời gian hoàn tiền.",
                ground_truth="Cần nêu: thời hạn 30 ngày, sản phẩm chưa sử dụng, hóa đơn, hoàn tiền 3-5 ngày làm việc"
            ),
        ])
        
        # Code Generation Cases
        self.test_cases.extend([
            TestCase(
                category=TaskCategory.CODE_GENERATION,
                prompt="Viết Python function để đọc file CSV, tính tổng và trung bình của cột 'revenue', và trả về dict với các key: total, average, count. Xử lý error cases.",
                ground_truth="Cần có: try-except, pandas/csv, validation, return dict với 3 key"
            ),
            TestCase(
                category=TaskCategory.CODE_GENERATION,
                prompt="Implement decorator @retry(max_attempts=3, delay=1) trong Python. Khi function raise exception, retry đến max_attempts lần với delay giây.",
                ground_truth="Cần có: functools.wraps, loop retry, delay, exception propagation"
            ),
        ])
        
        # RAG Answering Cases (thực tế doanh nghiệp)
        self.test_cases.extend([
            TestCase(
                category=TaskCategory.RAG_ANSWERING,
                prompt="Dựa trên tài liệu sau, trả lời: 'Chính sách bảo hành của sản phẩm điện tử là bao lâu?'\n\nTài liệu: Công ty XYZ cung cấp bảo hành 24 tháng cho tất cả sản phẩm điện tử. Bảo hành bao gồm lỗi từ nhà sản xuất. Không bao gồm hư hỏng do người dùng. Để kích hoạt bảo hành, cần giữ hóa đơn mua hàng.",
                context="XYZ Electronics Warranty Policy v1.2",
                ground_truth="Cần trả lời: 24 tháng, lỗi nhà sản xuất, cần hóa đơn"
            ),
            TestCase(
                category=TaskCategory.RAG_ANSWERING,
                prompt="Tìm thông tin về thủ tục đăng ký tài khoản doanh nghiệp mới.\n\nTài liệu: Đăng ký doanh nghiệp tại Việt Nam cần: 1) Giấy đề nghị đăng ký doanh nghiệp, 2) Điều lệ công ty, 3) Danh sách thành viên/cổ đông, 4) Bản sao CCCD của người đại diện, 5) Giấy ủy quyền nếu có. Thời gian xử lý: 3-5 ngày làm việc.",
                context="Vietnam Business Registration Guide 2024",
                ground_truth="Cần liệt kê đủ 5 giấy tờ và thời gian 3-5 ngày"
            ),
        ])
        
        # Summarization
        self.test_cases.extend([
            TestCase(
                category=TaskCategory.SUMMARIZATION,
                prompt="Tóm tắt bài báo sau thành 3 bullet points:\n\nCông nghệ AI đang thay đổi cách các doanh nghiệp vận hành. Theo báo cáo của McKinsey năm 2024, 67% công ty Fortune 500 đã triển khai ít nhất một giải pháp AI vào quy trình sản xuất. Điều này dẫn đến tăng trưởng năng suất trung bình 35% và giảm chi phí vận hành 25%. Tuy nhiên, việc áp dụng AI cũng đặt ra thách thức về đạo đức và quyền riêng tư dữ liệu.",
                ground_truth="Cần có: % công ty áp dụng, tăng trưởng năng suất, giảm chi phí, thách thức đạo đức"
            ),
        ])
        
        # Reasoning
        self.test_cases.extend([
            TestCase(
                category=TaskCategory.REASONING,
                prompt="Một cửa hàng bán 100 sản phẩm với giá 50.000đ/sản phẩm. Chi phí sản xuất mỗi sản phẩm là 30.000đ. Họ phải trả tiền thuê mặt bằng 500.000đ/ngày. Hỏi lợi nhuận ròng nếu bán hết trong 1 ngày?",
                ground_truth="Doanh thu: 5.000.000đ, Chi phí sản xuất: 3.000.000đ, Chi phí thuê: 500.000đ, Lợi nhuận: 1.500.000đ"
            ),
        ])
    
    def get_cases_by_category(self, category: TaskCategory) -> List[TestCase]:
        return [tc for tc in self.test_cases if tc.category == category]
    
    def export_to_json(self, filepath: str):
        data = {
            "total_cases": len(self.test_cases),
            "categories": {},
            "cases": []
        }
        for tc in self.test_cases:
            if tc.category.value not in data["categories"]:
                data["categories"][tc.category.value] = 0
            data["categories"][tc.category.value] += 1
            data["cases"].append({
                "id": tc.id,
                "category": tc.category.value,
                "prompt": tc.prompt,
                "context": tc.context,
                "ground_truth": tc.ground_truth
            })
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

Sử dụng

library = PromptLibrary() library.export_to_json("test_cases.json") print(f"Loaded {len(library.test_cases)} test cases") for category in TaskCategory: count = len(library.get_cases_by_category(category)) if count > 0: print(f" {category.value}: {count} cases")

Module 2: HolySheep API Client và Parallel Runner

Đây là phần cốt lõi - tôi sử dụng HolySheep AI để gọi đồng thời GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash và DeepSeek V3.2 qua cùng một endpoint. Chi phí chỉ từ $0.42/MTok với DeepSeek - rẻ hơn 35 lần so với Claude.

import os
import asyncio
import time
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from datetime import datetime
import httpx
from concurrent.futures import ThreadPoolExecutor
import pandas as pd
from tqdm import tqdm

Load environment

from dotenv import load_dotenv load_dotenv() @dataclass class ModelConfig: """Cấu hình model - map internal name sang HolySheep model""" internal_name: str holysheep_model: str cost_per_mtok_input: float # USD cost_per_mtok_output: float # USD avg_input_tokens_per_prompt: int = 500 avg_output_tokens_per_response: int = 800

Bảng giá HolySheep 2026 (tỷ giá ¥1=$1)

MODEL_CONFIGS = { "gpt4.1": ModelConfig( internal_name="gpt4.1", holysheep_model="gpt-4.1", cost_per_mtok_input=2.0, cost_per_mtok_output=8.0, avg_input_tokens_per_prompt=500, avg_output_tokens_per_response=800 ), "claude_sonnet_4.5": ModelConfig( internal_name="claude_sonnet_4.5", holysheep_model="claude-sonnet-4.5", cost_per_mtok_input=3.0, cost_per_mtok_output=15.0, avg_input_tokens_per_prompt=500, avg_output_tokens_per_response=800 ), "gemini_2.5_flash": ModelConfig( internal_name="gemini_2.5_flash", holysheep_model="gemini-2.5-flash", cost_per_mtok_input=0.30, cost_per_mtok_output=2.50, avg_input_tokens_per_prompt=500, avg_output_tokens_per_response=800 ), "deepseek_v3.2": ModelConfig( internal_name="deepseek_v3.2", holysheep_model="deepseek-v3.2", cost_per_mtok_input=0.07, cost_per_mtok_output=0.42, avg_input_tokens_per_prompt=500, avg_output_tokens_per_response=800 ), } @dataclass class APIResponse: """Response từ API call""" model: str prompt: str response: str input_tokens: int output_tokens: int latency_ms: float cost_input: float cost_output: float total_cost: float success: bool error: Optional[str] = None timestamp: str = field(default_factory=lambda: datetime.now().isoformat()) class HolySheepBenchmarkClient: """Client benchmark sử dụng HolySheep API - tiết kiệm 85%+ chi phí""" BASE_URL = "https://api.holysheep.ai/v1" def __init__(self, api_key: str): self.api_key = api_key self.client = httpx.Client( base_url=self.BASE_URL, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, timeout=60.0 ) def call_model(self, model_config: ModelConfig, prompt: str, context: Optional[str] = None) -> APIResponse: """Gọi single model với prompt""" start_time = time.time() full_prompt = prompt if context: full_prompt = f"Context: {context}\n\nQuestion: {prompt}" try: response = self.client.post( "/chat/completions", json={ "model": model_config.holysheep_model, "messages": [{"role": "user", "content": full_prompt}], "temperature": 0.7, "max_tokens": 2000 } ) elapsed_ms = (time.time() - start_time) * 1000 if response.status_code == 200: data = response.json() content = data["choices"][0]["message"]["content"] usage = data.get("usage", {}) input_tokens = usage.get("prompt_tokens", model_config.avg_input_tokens_per_prompt) output_tokens = usage.get("completion_tokens", model_config.avg_output_tokens_per_response) cost_input = (input_tokens / 1000) * model_config.cost_per_mtok_input cost_output = (output_tokens / 1000) * model_config.cost_per_mtok_output return APIResponse( model=model_config.internal_name, prompt=prompt, response=content, input_tokens=input_tokens, output_tokens=output_tokens, latency_ms=elapsed_ms, cost_input=cost_input, cost_output=cost_output, total_cost=cost_input + cost_output, success=True ) else: return APIResponse( model=model_config.internal_name, prompt=prompt, response="", input_tokens=0, output_tokens=0, latency_ms=elapsed_ms, cost_input=0, cost_output=0, total_cost=0, success=False, error=f"HTTP {response.status_code}: {response.text}" ) except Exception as e: elapsed_ms = (time.time() - start_time) * 1000 return APIResponse( model=model_config.internal_name, prompt=prompt, response="", input_tokens=0, output_tokens=0, latency_ms=elapsed_ms, cost_input=0, cost_output=0, total_cost=0, success=False, error=str(e) ) def benchmark_models(self, test_cases: List[TestCase], models: List[str] = None) -> pd.DataFrame: """Chạy benchmark trên tất cả models cho tất cả test cases""" if models is None: models = list(MODEL_CONFIGS.keys()) results = [] total_runs = len(test_cases) * len(models) print(f"🚀 Starting benchmark: {len(test_cases)} cases × {len(models)} models = {total_runs} runs") with tqdm(total=total_runs, desc="Benchmarking") as pbar: for test_case in test_cases: for model_key in models: model_config = MODEL_CONFIGS[model_key] result = self.call_model( model_config, test_case.prompt, test_case.context ) results.append({ "test_case_id": test_case.id, "category": test_case.category.value, "model": result.model, "response": result.response, "input_tokens": result.input_tokens, "output_tokens": result.output_tokens, "latency_ms": result.latency_ms, "cost": result.total_cost, "success": result.success, "error": result.error, "ground_truth": test_case.ground_truth, "timestamp": result.timestamp }) pbar.update(1) # Rate limiting nhẹ để tránh quota limit time.sleep(0.1) return pd.DataFrame(results) def benchmark_parallel(self, test_cases: List[TestCase], models: List[str] = None, max_workers: int = 5) -> pd.DataFrame: """Chạy benchmark song song - nhanh hơn 5 lần""" if models is None: models = list(MODEL_CONFIGS.keys()) results = [] tasks = [] for test_case in test_cases: for model_key in models: model_config = MODEL_CONFIGS[model_key] tasks.append((model_config, test_case)) print(f"🚀 Parallel benchmark: {len(tasks)} tasks with {max_workers} workers") def process_task(args): config, tc = args return self.call_model(config, tc.prompt, tc.context), tc with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [executor.submit(process_task, task) for task in tasks] for future in tqdm(futures, desc="Parallel benchmarking"): result, test_case = future.result() results.append({ "test_case_id": test_case.id, "category": test_case.category.value, "model": result.model, "response": result.response, "input_tokens": result.input_tokens, "output_tokens": result.output_tokens, "latency_ms": result.latency_ms, "cost": result.total_cost, "success": result.success, "error": result.error, "ground_truth": test_case.ground_truth, "timestamp": result.timestamp }) return pd.DataFrame(results)

Sử dụng

api_key = os.getenv("HOLYSHEEP_API_KEY") client = HolySheepBenchmarkClient(api_key) library = PromptLibrary()

Chạy benchmark (sequential - chậm nhưng ổn định)

df_results = client.benchmark_models(library.test_cases[:5])

Hoặc chạy parallel - nhanh hơn

df_results = client.benchmark_parallel(library.test_cases[:20], max_workers=5)

df_results.to_csv("benchmark_results.csv", index=False) print(f"✅ Benchmark completed. Results saved to benchmark_results.csv")

Module 3: Scoring Engine với Đa phương pháp

Tôi sử dụng 3 phương pháp scoring khác nhau để có đánh giá toàn diện: LLM-as-Judge, ROUGE/BLEU metrics, và semantic similarity. Kết hợp lại cho score tổng hợp đáng tin cậy.

import re
from typing import List, Dict, Tuple
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

class ScoringEngine:
    """Engine đánh giá chất lượng response đa chiều"""
    
    def __init__(self, judge_model: str = "gpt-4.1"):
        self.judge_model = judge_model
        self.tfidf = TfidfVectorizer(stop_words=None)  # Vietnamese compatible
    
    def extract_keywords(self, text: str) -> set:
        """Trích xuất keywords từ text"""
        # Simple tokenization cho tiếng Việt
        words = re.findall(r'\b\w+\b', text.lower())
        # Filter stopwords đơn giản
        stopwords = {'và', 'của', 'là', 'có', 'được', 'trong', 'cho', 'với', 'để', 'từ', 'các', 'theo'}
        return set(w for w in words if len(w) > 2 and w not in stopwords)
    
    def keyword_recall(self, response: str, ground_truth: str) -> float:
        """Tính keyword recall - bao nhiêu keywords từ ground truth xuất hiện trong response"""
        if not ground_truth:
            return 0.5  # Neutral if no ground truth
        
        gt_keywords = self.extract_keywords(ground_truth)
        resp_keywords = self.extract_keywords(response)
        
        if not gt_keywords:
            return 0.5
        
        matches = len(gt_keywords.intersection(resp_keywords))
        return matches / len(gt_keywords)
    
    def semantic_similarity(self, response: str, ground_truth: str) -> float:
        """Tính semantic similarity sử dụng TF-IDF cosine"""
        if not ground_truth or not response:
            return 0.0
        
        try:
            vectors = self.tfidf.fit_transform([response, ground_truth])
            similarity = cosine_similarity(vectors[0:1], vectors[1:2])[0][0]
            return float(similarity)
        except:
            return self.keyword_recall(response, ground_truth)  # Fallback
    
    def response_completeness(self, response: str, ground_truth: str) -> float:
        """Đánh giá response có đầy đủ thông tin không"""
        if not ground_truth:
            return 0.5
        
        # Check required elements (trích từ ground truth format)
        elements = re.findall(r'\(([^)]+)\)', ground_truth)
        score = 0.0
        
        for element in elements:
            # Simple presence check
            if element.lower() in response.lower():
                score += 1.0
        
        if elements:
            return score / len(elements)
        return 0.5
    
    def response_quality(self, response: str) -> float:
        """Đánh giá chất lượng response thuần túy"""
        score = 0.0
        
        # Length check (reasonable response)
        word_count = len(response.split())
        if 50 <= word_count <= 500:
            score += 0.3
        elif word_count >= 20:
            score += 0.2
        
        # Structure check
        if any(marker in response for marker in ['.', '!', '?']):
            score += 0.2
        
        # Has specific information
        if any(char.isdigit() for char in response):
            score += 0.2
        
        # No obvious errors
        if not re.search(r'[A-Z]{10,}', response):  # No long caps lock
            score += 0.3
        
        return min(score, 1.0)
    
    def calculate_composite_score(self, response: str, ground_truth: str,
                                  weights: Dict[str, float] = None) -> Dict[str, float]:
        """Tính composite score từ nhiều metrics"""
        
        if weights is None:
            weights = {
                "keyword_recall": 0.25,
                "semantic_similarity": 0.25,
                "completeness": 0.25,
                "response_quality": 0.25
            }
        
        scores = {
            "keyword_recall": self.keyword_recall(response, ground_truth),
            "semantic_similarity": self.semantic_similarity(response, ground_truth),
            "completeness": self.response_completeness(response, ground_truth),
            "response_quality": self.response_quality(response)
        }
        
        # Weighted composite
        composite = sum(scores[k] * weights[k] for k in weights)
        scores["composite"] = composite
        
        return scores
    
    def score_results(self, df_results: pd.DataFrame) -> pd.DataFrame:
        """Đánh score cho toàn bộ benchmark results"""
        
        all_scores = []
        
        for idx, row in df_results.iterrows():
            if row["success"]:
                scores = self.calculate_composite_score(
                    row["response"],
                    row.get("ground_truth", "")
                )
            else:
                scores = {
                    "keyword_recall": 0.0,
                    "semantic_similarity": 0.0,
                    "completeness": 0.0,
                    "response_quality": 0.0,
                    "composite": 0.0
                }
            
            scores["test_case_id"] = row["test_case_id"]
            scores["model"] = row["model"]
            scores["cost"] = row["cost"]
            scores["latency_ms"] = row["latency_ms"]
            all_scores.append(scores)
        
        df_scores = pd.DataFrame(all_scores)
        return df_results.merge(df_scores, on=["test_case_id", "model"])
    
    def generate_report(self, df_scored: pd.DataFrame) -> Dict:
        """Generate benchmark report tổng hợp"""
        
        report = {
            "timestamp": datetime.now().isoformat(),
            "summary": {},
            "by_category": {},
            "by_model": {},
            "cost_analysis": {},
            "recommendations": []
        }
        
        # Overall summary
        report["summary"] = {
            "total_runs": len(df_scored),
            "successful_runs": len(df_scored[df_scored["success"] == True]),
            "avg_latency_ms": df_scored["latency_ms"].mean(),
            "avg_cost_per_call": df_scored["cost"].mean(),
            "avg_composite_score": df_scored["composite"].mean()
        }
        
        # By model
        for model in df_scored["model"].unique():
            model_data = df_scored[df_scored["model"] == model]
            report["by_model"][model] = {
                "avg_score": model_data["composite"].mean(),
                "avg_latency_ms": model_data["latency_ms"].mean(),
                "avg_cost": model_data["cost"].mean(),
                "success_rate": model_data["success"].mean() * 100,
                "runs": len(model_data)
            }
        
        # By category
        for category in df_scored["category"].unique():
            cat_data = df_scored[df_scored["category"] == category]
            scores_by_model = cat_data.groupby("model")["composite"].mean()
            best_model = scores_by_model.idxmax()
            report["by_category"][category] = {
                "best_model": best_model,
                "best_score": scores_by_model.max(),
                "avg_latency_ms": cat_data["latency_ms"].mean(),
                "total_cost": cat_data["cost"].sum()
            }
        
        # Cost analysis
        total_cost = df_scored["cost"].sum()
        cost_by_model = df_scored.groupby("model")["cost"].sum()
        
        report["cost_analysis"] = {
            "total_benchmark_cost": total_cost,
            "by_model": cost_by_model.to_dict(),
            "potential_savings": {}
        }
        
        # Find cheapest model with good score
        good_enough_score = df_scored["composite"].quantile(0.8)
        cheap_models = df_scored[df_scored["composite"] >= good_enough_score]
        
        if len(cheap_models) > 0:
            cheapest = cheap_models.groupby("model")["cost"].mean().idxmin()
            expensive = df_scored["cost"].mean()
            cheap = cheap_models["cost"].mean()
            report["cost_analysis"]["potential_savings"] = {
                "cheapest_good_model": cheapest,
                "savings_percent": ((expensive - cheap) / expensive) * 100
            }
        
        # Recommendations
        for model, stats in report["by_model"].items():
            if stats["avg_score"] >= 0.8 and stats["avg_cost"] < 0.01:
                report["recommendations"].append({
                    "type": "best_value",
                    "model": model,
                    "reason": f"Score {stats['avg_score']:.2f} with cost ${stats['avg_cost']:.4f}"
                })
        
        return report

Sử dụng

scorer = ScoringEngine() df_scored = scorer.score_results(df_results) df_scored.to_csv("benchmark_scored.csv", index=False) report = scorer.generate_report(df_scored) print(json.dumps(report, indent=2))

Module 4: Visualization và Report Generator

import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime

class BenchmarkVisualizer:
    """Visualize benchmark results"""
    
    def __init__(self, df_scored: pd.DataFrame, report: Dict):
        self.df = df_scored
        self.report = report
        self.colors = {
            "gpt4.1": "#10a37f",
            "claude_sonnet_4.5": "#d4a574",
            "gemini_2.5_flash": "#4285f4",
            "deepseek_v3.2": "#ff6b6b"
        }
    
    def plot_score_comparison(self, save_path: str = "scores_comparison.png"):
        """Vẽ biểu đồ so sánh điểm số giữa các model"""
        fig, axes = plt.subplots(1, 2, figsize=(14, 5))
        
        # Composite score by model
        scores = self.df.groupby("model")["composite"].mean().sort_values(ascending=False)
        axes[0].bar