#!/usr/bin/env python3
"""
高级优化版分联赛大小球预测模型
- 集成多个模型
- 添加H2H特征
- 添加更多滚动特征
- 超参数调优
"""

import os
import json
import math
from collections import defaultdict
from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd
import joblib
from sklearn.ensemble import (
    RandomForestClassifier, GradientBoostingClassifier, 
    ExtraTreesClassifier, VotingClassifier, StackingClassifier
)
from sklearn.impute import SimpleImputer
from sklearn.metrics import accuracy_score
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression

REPO_ROOT = Path(__file__).resolve().parents[1]
HIST_DIR = REPO_ROOT / 'historical_data'
ARTIFACT_DIR = REPO_ROOT / 'experiments' / 'goal_number_advanced'
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)

LEAGUE_ZH_MAP = {
    'a1': '澳超', 'br1': '巴甲', 'cz1': '捷克甲', 'd1': '德甲', 'd2': '德乙',
    'e0': '英超', 'e1': '英甲', 'e2': '英冠', 'e3': '英乙', 'e2n': '荷乙',
    'f1': '法甲', 'f2': '法乙', 'g1': '希腊超', 'i1': '意甲', 'j1': '日职联',
    'j2': '日职乙', 'k2': '韩国K2', 'm1': '美职联', 'p1': '葡超', 'r1': '俄超',
    'sc1': '苏超', 'sp1': '西甲', 'tr1': '土超', 'uz1': '乌兹别克超',
}

def mean_or_default(values, default):
    return float(np.mean(values)) if values else float(default)

def std_or_default(values, default=0):
    return float(np.std(values, ddof=0)) if len(values) >= 2 else float(default)

def extract_prob(odds):
    if odds is None or odds <= 0 or pd.isna(odds): return np.nan
    return 1.0 / float(odds)

# 完整特征列表
FEATURE_COLS = [
    # O/U odds
    'over_odds', 'under_odds', 'over_implied_prob', 'under_implied_prob', 'odds_margin',
    # Basic stats
    'home_gf_avg', 'home_ga_avg', 'away_gf_avg', 'away_ga_avg',
    'home_tot_avg', 'away_tot_avg', 'home_over_rate', 'away_over_rate',
    # Home/Away specific
    'home_home_gf_avg', 'home_home_ga_avg', 'home_home_over_rate',
    'away_away_gf_avg', 'away_away_ga_avg', 'away_away_over_rate',
    # Rolling
    'home_gf_last5', 'home_ga_last5', 'away_gf_last5', 'away_ga_last5',
    'home_over_last5', 'away_over_last5',
    'home_gf_last3', 'home_ga_last3', 'away_gf_last3', 'away_ga_last3',
    'home_over_last3', 'away_over_last3',
    # League
    'league_avg_tot', 'league_avg_over_rate', 'league_over_rate_std',
    # Derived
    'expected_goals_proxy', 'combined_over_rate',
    'form_diff', 'goals_diff', 'over_rate_diff',
    # Std
    'home_gf_std', 'away_gf_std', 'home_ga_std', 'away_ga_std',
    'home_tot_std', 'away_tot_std',
    # Streak
    'home_over_streak', 'away_over_streak',
    'home_under_streak', 'away_under_streak',
    # Trend
    'home_form_trend', 'away_form_trend',
    # Comparison
    'implied_vs_hist_over',
    # H2H features
    'h2h_over_rate', 'h2h_total_avg', 'h2h_home_over_rate', 'h2h_away_over_rate',
    # Rolling variance
    'home_gf_rolling_std', 'away_gf_rolling_std',
    # Matchup specific
    'home_away_gf_avg', 'away_home_ga_avg',
    # Odds movement proxy
    'odds_vs_league_avg',
]

def build_features(home_hist, away_hist, league_hist, h2h_hist, over_odds, under_odds):
    league_tot_avg = mean_or_default(league_hist['tot'], 2.6)
    league_over_rate = mean_or_default(league_hist['over'], 0.5)
    league_over_std = std_or_default(league_hist['over'], 0.15)
    
    home_gf_avg = mean_or_default(home_hist['gf'], 1.4)
    home_ga_avg = mean_or_default(home_hist['ga'], 1.4)
    away_gf_avg = mean_or_default(away_hist['gf'], 1.2)
    away_ga_avg = mean_or_default(away_hist['ga'], 1.2)
    home_tot_avg = mean_or_default(home_hist['tot'], league_tot_avg)
    away_tot_avg = mean_or_default(away_hist['tot'], league_tot_avg)
    home_over_rate = mean_or_default(home_hist['over'], 0.5)
    away_over_rate = mean_or_default(away_hist['over'], 0.5)
    
    # Home/Away specific
    home_home_gf_avg = mean_or_default(home_hist['home_gf'], home_gf_avg)
    home_home_ga_avg = mean_or_default(home_hist['home_ga'], home_ga_avg)
    home_home_over_rate = mean_or_default(home_hist['home_over'], home_over_rate)
    away_away_gf_avg = mean_or_default(away_hist['away_gf'], away_gf_avg)
    away_away_ga_avg = mean_or_default(away_hist['away_ga'], away_ga_avg)
    away_away_over_rate = mean_or_default(away_hist['away_over'], away_over_rate)
    
    # Rolling
    home_gf_last5 = mean_or_default(home_hist['gf'][-5:], home_gf_avg)
    home_ga_last5 = mean_or_default(home_hist['ga'][-5:], home_ga_avg)
    away_gf_last5 = mean_or_default(away_hist['gf'][-5:], away_gf_avg)
    away_ga_last5 = mean_or_default(away_hist['ga'][-5:], away_ga_avg)
    home_over_last5 = mean_or_default(home_hist['over'][-5:], home_over_rate)
    away_over_last5 = mean_or_default(away_hist['over'][-5:], away_over_rate)
    
    home_gf_last3 = mean_or_default(home_hist['gf'][-3:], home_gf_avg)
    home_ga_last3 = mean_or_default(home_hist['ga'][-3:], home_ga_avg)
    away_gf_last3 = mean_or_default(away_hist['gf'][-3:], away_gf_avg)
    away_ga_last3 = mean_or_default(away_hist['ga'][-3:], away_ga_avg)
    home_over_last3 = mean_or_default(home_hist['over'][-3:], home_over_rate)
    away_over_last3 = mean_or_default(away_hist['over'][-3:], away_over_rate)
    
    # Std
    home_gf_std = std_or_default(home_hist['gf'], 1.0)
    away_gf_std = std_or_default(away_hist['gf'], 1.0)
    home_ga_std = std_or_default(home_hist['ga'], 1.0)
    away_ga_std = std_or_default(away_hist['ga'], 1.0)
    home_tot_std = std_or_default(home_hist['tot'], 1.0)
    away_tot_std = std_or_default(away_hist['tot'], 1.0)
    
    # Rolling std
    home_gf_rolling_std = std_or_default(home_hist['gf'][-10:] if len(home_hist['gf']) >= 10 else home_hist['gf'], 1.0)
    away_gf_rolling_std = std_or_default(away_hist['gf'][-10:] if len(away_hist['gf']) >= 10 else away_hist['gf'], 1.0)
    
    # Streaks
    def calc_streak(values, target=1):
        streak = 0
        for v in reversed(values):
            if v == target: streak += 1
            else: break
        return streak
    
    home_over_streak = calc_streak(home_hist['over'], 1)
    away_over_streak = calc_streak(away_hist['over'], 1)
    home_under_streak = calc_streak(home_hist['over'], 0)
    away_under_streak = calc_streak(away_hist['over'], 0)
    
    # Trend
    home_form_trend = home_over_last3 - home_over_last5
    away_form_trend = away_over_last3 - away_over_last5
    
    # Implied
    over_implied = extract_prob(over_odds)
    implied_vs_hist = over_implied - home_over_rate if not math.isnan(over_implied) else 0
    
    # Expected
    expected_goals_proxy = np.nanmean([home_gf_avg, away_ga_avg])
    combined_over_rate = np.nanmean([home_over_rate, away_over_rate])
    
    # H2H
    h2h_over_rate = mean_or_default(h2h_hist['over'], 0.5) if h2h_hist else 0.5
    h2h_total_avg = mean_or_default(h2h_hist['tot'], league_tot_avg) if h2h_hist else league_tot_avg
    h2h_home_over_rate = mean_or_default(h2h_hist['home_over'], h2h_over_rate) if h2h_hist else h2h_over_rate
    h2h_away_over_rate = mean_or_default(h2h_hist['away_over'], h2h_over_rate) if h2h_hist else h2h_over_rate
    
    # Matchup specific
    home_away_gf_avg = mean_or_default(home_hist.get('away_gf', []), home_gf_avg)
    away_home_ga_avg = mean_or_default(away_hist.get('home_ga', []), away_ga_avg)
    
    # Odds vs league avg
    league_avg_odds = 1 / ((1/league_tot_avg) * (1 - league_over_rate) + league_over_rate) if league_over_rate != 0.5 else 2.0
    odds_vs_league = (1/over_odds) - league_avg_odds if over_odds > 0 else 0
    
    return {
        'over_odds': over_odds, 'under_odds': under_odds,
        'over_implied_prob': over_implied,
        'under_implied_prob': extract_prob(under_odds),
        'odds_margin': (over_implied + extract_prob(under_odds)) - 1 if not (math.isnan(over_implied) or math.isnan(extract_prob(under_odds))) else 0,
        'home_gf_avg': home_gf_avg, 'home_ga_avg': home_ga_avg,
        'away_gf_avg': away_gf_avg, 'away_ga_avg': away_ga_avg,
        'home_tot_avg': home_tot_avg, 'away_tot_avg': away_tot_avg,
        'home_over_rate': home_over_rate, 'away_over_rate': away_over_rate,
        'home_home_gf_avg': home_home_gf_avg, 'home_home_ga_avg': home_home_ga_avg,
        'home_home_over_rate': home_home_over_rate,
        'away_away_gf_avg': away_away_gf_avg, 'away_away_ga_avg': away_away_ga_avg,
        'away_away_over_rate': away_away_over_rate,
        'home_gf_last5': home_gf_last5, 'home_ga_last5': home_ga_last5,
        'away_gf_last5': away_gf_last5, 'away_ga_last5': away_ga_last5,
        'home_over_last5': home_over_last5, 'away_over_last5': away_over_last5,
        'home_gf_last3': home_gf_last3, 'home_ga_last3': home_ga_last3,
        'away_gf_last3': away_gf_last3, 'away_ga_last3': away_ga_last3,
        'home_over_last3': home_over_last3, 'away_over_last3': away_over_last3,
        'league_avg_tot': league_tot_avg, 'league_avg_over_rate': league_over_rate,
        'league_over_rate_std': league_over_std,
        'expected_goals_proxy': expected_goals_proxy, 'combined_over_rate': combined_over_rate,
        'form_diff': home_over_rate - away_over_rate,
        'goals_diff': (home_gf_avg + away_ga_avg) - (away_gf_avg + home_ga_avg),
        'over_rate_diff': home_over_rate - away_over_rate,
        'home_gf_std': home_gf_std, 'away_gf_std': away_gf_std,
        'home_ga_std': home_ga_std, 'away_ga_std': away_ga_std,
        'home_tot_std': home_tot_std, 'away_tot_std': away_tot_std,
        'home_over_streak': home_over_streak, 'away_over_streak': away_over_streak,
        'home_under_streak': home_under_streak, 'away_under_streak': away_under_streak,
        'home_form_trend': home_form_trend, 'away_form_trend': away_form_trend,
        'implied_vs_hist_over': implied_vs_hist,
        'h2h_over_rate': h2h_over_rate,
        'h2h_total_avg': h2h_total_avg,
        'h2h_home_over_rate': h2h_home_over_rate,
        'h2h_away_over_rate': h2h_away_over_rate,
        'home_gf_rolling_std': home_gf_rolling_std,
        'away_gf_rolling_std': away_gf_rolling_std,
        'home_away_gf_avg': home_away_gf_avg,
        'away_home_ga_avg': away_home_ga_avg,
        'odds_vs_league_avg': odds_vs_league,
    }

def find_best_ou_cols(df):
    candidates = [('B365>2.5', 'B365<2.5'), ('Avg>2.5', 'Avg<2.5'),
                  ('Max>2.5', 'Max<2.5'), ('P>2.5', 'P<2.5')]
    for over_col, under_col in candidates:
        if over_col in df.columns and under_col in df.columns:
            valid = df.dropna(subset=[over_col, under_col, 'FTHG', 'FTAG'])
            if len(valid) > 100: return over_col, under_col
    return None, None

def parse_date(d):
    if pd.isna(d): return None
    d = str(d)
    for fmt in ['%d/%m/%Y', '%Y-%m-%d', '%d/%m/%y']:
        try: return datetime.strptime(d[:10], fmt)
        except: continue
    return None

def main():
    print("="*70)
    print("高级优化版分联赛大小球预测模型")
    print("="*70)
    
    hist_files = sorted(HIST_DIR.glob('*_history.csv'))
    print(f"\n找到 {len(hist_files)} 个历史数据文件")
    
    all_results = {}
    
    for hist_path in hist_files:
        league_code = hist_path.stem.replace('_history', '')
        league_name = LEAGUE_ZH_MAP.get(league_code, league_code)
        
        print(f"\n{'='*60}")
        print(f"处理: {league_name} ({league_code})")
        print("="*60)
        
        try:
            df = pd.read_csv(hist_path, low_memory=False)
        except Exception as e:
            print(f"  读取失败: {e}")
            continue
        
        over_col, under_col = find_best_ou_cols(df)
        if not over_col:
            print(f"  无O/U数据，跳过")
            continue
        
        df = df.dropna(subset=[over_col, under_col, 'FTHG', 'FTAG', 'HomeTeam', 'AwayTeam'])
        df = df[df[over_col] > 1]
        
        if len(df) < 300:
            print(f"  数据不足: {len(df)} 场")
            continue
        
        df['parsed_date'] = df['Date'].apply(parse_date)
        df = df.dropna(subset=['parsed_date'])
        df = df.sort_values('parsed_date').reset_index(drop=True)
        
        if len(df) < 300:
            print(f"  日期解析后数据不足: {len(df)} 场")
            continue
        
        print(f"  数据: {len(df)} 场")
        
        # 转换数值
        df['over_odds'] = pd.to_numeric(df[over_col], errors='coerce')
        df['under_odds'] = pd.to_numeric(df[under_col], errors='coerce')
        df['home_goals'] = pd.to_numeric(df['FTHG'], errors='coerce').astype(int)
        df['away_goals'] = pd.to_numeric(df['FTAG'], errors='coerce').astype(int)
        df = df.dropna(subset=['over_odds', 'under_odds', 'home_goals', 'away_goals'])
        df['total_goals'] = df['home_goals'] + df['away_goals']
        df['is_over'] = (df['total_goals'] > 2.5).astype(int)
        
        split_idx = int(len(df) * 0.7)
        train_df = df.iloc[:split_idx]
        test_df = df.iloc[split_idx:]
        
        if len(train_df) < 150 or len(test_df) < 50:
            print(f"  数据不足，跳过")
            continue
        
        print(f"  训练: {len(train_df)} 场, 测试: {len(test_df)} 场")
        
        # 构建特征
        team_hist = defaultdict(lambda: {'gf': [], 'ga': [], 'tot': [], 'over': [], 
                                         'home_gf': [], 'home_ga': [], 'home_over': [],
                                         'away_gf': [], 'away_ga': [], 'away_over': []})
        league_hist = defaultdict(lambda: {'tot': [], 'over': []})
        h2h_hist = defaultdict(lambda: {'tot': [], 'over': [], 'home_over': [], 'away_over': []})
        
        train_features = []
        train_labels = []
        
        for _, row in train_df.iterrows():
            home_key = (league_code, str(row['HomeTeam']))
            away_key = (league_code, str(row['AwayTeam']))
            matchup_key = (str(row['HomeTeam']), str(row['AwayTeam']))
            hh = team_hist[home_key]
            aa = team_hist[away_key]
            ll = league_hist[league_code]
            h2h = h2h_hist[matchup_key]
            
            features = build_features(hh, aa, ll, h2h, row['over_odds'], row['under_odds'])
            train_features.append(features)
            train_labels.append(row['is_over'])
            
            # Update
            total = row['total_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals']))
            hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total)
            hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals']))
            hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals']))
            aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total)
            aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals']))
            aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total)
            ll['over'].append(float(is_over))
            h2h['tot'].append(total)
            h2h['over'].append(float(is_over))
            h2h['home_over'].append(float(is_over))
            h2h['away_over'].append(float(is_over))
        
        X_train = pd.DataFrame(train_features)
        available_cols = [c for c in FEATURE_COLS if c in X_train.columns]
        X_train = X_train[available_cols]
        y_train = np.array(train_labels)
        
        print(f"  特征数: {len(available_cols)}")
        
        # Build test features
        team_hist = defaultdict(lambda: {'gf': [], 'ga': [], 'tot': [], 'over': [], 
                                         'home_gf': [], 'home_ga': [], 'home_over': [],
                                         'away_gf': [], 'away_ga': [], 'away_over': []})
        league_hist = defaultdict(lambda: {'tot': [], 'over': []})
        h2h_hist = defaultdict(lambda: {'tot': [], 'over': [], 'home_over': [], 'away_over': []})
        
        # Use train data to build history first
        for _, row in train_df.iterrows():
            home_key = (league_code, str(row['HomeTeam']))
            away_key = (league_code, str(row['AwayTeam']))
            matchup_key = (str(row['HomeTeam']), str(row['AwayTeam']))
            hh = team_hist[home_key]
            aa = team_hist[away_key]
            ll = league_hist[league_code]
            h2h = h2h_hist[matchup_key]
            total = row['total_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals']))
            hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total)
            hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals']))
            hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals']))
            aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total)
            aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals']))
            aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total)
            ll['over'].append(float(is_over))
            h2h['tot'].append(total)
            h2h['over'].append(float(is_over))
            h2h['home_over'].append(float(is_over))
            h2h['away_over'].append(float(is_over))
        
        test_features = []
        test_labels = []
        
        for _, row in test_df.iterrows():
            home_key = (league_code, str(row['HomeTeam']))
            away_key = (league_code, str(row['AwayTeam']))
            matchup_key = (str(row['HomeTeam']), str(row['AwayTeam']))
            hh = team_hist[home_key]
            aa = team_hist[away_key]
            ll = league_hist[league_code]
            h2h = h2h_hist[matchup_key]
            
            features = build_features(hh, aa, ll, h2h, row['over_odds'], row['under_odds'])
            test_features.append(features)
            test_labels.append(row['is_over'])
            
            # Update
            total = row['total_goals']
            is_over = row['is_over']
            hh['gf'].append(float(row['home_goals']))
            hh['ga'].append(float(row['away_goals']))
            hh['tot'].append(total)
            hh['over'].append(float(is_over))
            hh['home_gf'].append(float(row['home_goals']))
            hh['home_ga'].append(float(row['away_goals']))
            hh['home_over'].append(float(is_over))
            aa['gf'].append(float(row['away_goals']))
            aa['ga'].append(float(row['home_goals']))
            aa['tot'].append(total)
            aa['over'].append(float(is_over))
            aa['away_gf'].append(float(row['away_goals']))
            aa['away_ga'].append(float(row['away_goals']))
            aa['away_over'].append(float(is_over))
            ll['tot'].append(total)
            ll['over'].append(float(is_over))
            h2h['tot'].append(total)
            h2h['over'].append(float(is_over))
            h2h['home_over'].append(float(is_over))
            h2h['away_over'].append(float(is_over))
        
        X_test = pd.DataFrame(test_features)[available_cols]
        y_test = np.array(test_labels)
        
        # Impute
        imputer = SimpleImputer(strategy='median')
        X_train_imp = imputer.fit_transform(X_train)
        X_test_imp = imputer.transform(X_test)
        
        baseline = y_test.mean()
        print(f"  基准大球率: {baseline:.1%}")
        
        # Define models
        models = {
            'ET_d12': ExtraTreesClassifier(n_estimators=300, max_depth=12, min_samples_leaf=3, random_state=42, class_weight='balanced', n_jobs=-1),
            'ET_d15': ExtraTreesClassifier(n_estimators=400, max_depth=15, min_samples_leaf=2, random_state=42, class_weight='balanced', n_jobs=-1),
            'RF_d10': RandomForestClassifier(n_estimators=500, max_depth=10, min_samples_leaf=3, random_state=42, class_weight='balanced', n_jobs=-1),
            'RF_d12': RandomForestClassifier(n_estimators=400, max_depth=12, min_samples_leaf=2, random_state=42, class_weight='balanced', n_jobs=-1),
            'GB': GradientBoostingClassifier(n_estimators=200, max_depth=5, learning_rate=0.05, random_state=42),
        }
        
        # Evaluate each model with different thresholds
        print(f"  测试各模型和阈值...")
        
        best_overall = {'accuracy': 0}
        
        for name, model in models.items():
            try:
                model.fit(X_train_imp, y_train)
                probs = model.predict_proba(X_test_imp)[:, 1]
                
                for threshold in [0.55, 0.58, 0.60, 0.62, 0.65, 0.68, 0.70]:
                    mask = (probs >= threshold) | (probs <= (1 - threshold))
                    if mask.sum() >= 15:
                        cp = probs[mask]; cl = y_test[mask]; cp2 = (cp >= threshold).astype(int)
                        acc = accuracy_score(cl, cp2)
                        over_m = cp2 == 1; under_m = cp2 == 0
                        over_acc = accuracy_score(cl[over_m], cp2[over_m]) if over_m.sum() > 0 else 0
                        under_acc = accuracy_score(cl[under_m], cp2[under_m]) if under_m.sum() > 0 else 0
                        
                        if acc > best_overall['accuracy']:
                            best_overall = {
                                'model': name,
                                'threshold': threshold,
                                'count': int(mask.sum()),
                                'accuracy': float(acc),
                                'over_count': int(over_m.sum()),
                                'over_accuracy': float(over_acc),
                                'under_count': int(under_m.sum()),
                                'under_accuracy': float(under_acc),
                            }
            except Exception as e:
                print(f"    Error with {name}: {e}")
        
        if best_overall['accuracy'] > 0:
            print(f"  最佳: {best_overall['model']} 阈值={best_overall['threshold']}: {best_overall['count']}场 准确率={best_overall['accuracy']:.1%}")
            print(f"       (大球{best_overall['over_count']}场{best_overall['over_accuracy']:.1%}, 小球{best_overall['under_count']}场{best_overall['under_accuracy']:.1%})")
            
            # Save best model
            best_model = models[best_overall['model']]
            joblib.dump((best_model, available_cols, imputer, best_overall['threshold']), ARTIFACT_DIR / f'{league_code}_model.pkl')
            
            all_results[league_code] = {
                'league': league_name,
                'train': len(train_df),
                'test': len(test_df),
                'baseline': float(baseline),
                **best_overall
            }
    
    # 汇总
    print("\n" + "="*70)
    print("所有联赛汇总")
    print("="*70)
    
    print(f"\n{'联赛':<10} {'训练':<6} {'测试':<6} {'基准':<8} {'模型':<12} {'阈值':<8} {'准确率':<10} {'大球详情'}")
    print("-"*80)
    
    for code, res in sorted(all_results.items(), key=lambda x: -x[1].get('accuracy', 0)):
        name = res['league'][:8]
        baseline = f"{res['baseline']:.1%}"
        model = res.get('model', '-')[:10]
        threshold = f"{res.get('threshold', 0):.2f}"
        acc = f"{res.get('accuracy', 0):.1%}"
        over = f"{res.get('over_count', 0)}场/{res.get('over_accuracy', 0):.1%}"
        print(f"{name:<10} {res['train']:<6} {res['test']:<6} {baseline:<8} {model:<12} {threshold:<8} {acc:<10} {over}")
    
    with open(ARTIFACT_DIR / 'results.json', 'w', encoding='utf-8') as f:
        json.dump(all_results, f, ensure_ascii=False, indent=2)
    
    print(f"\n结果已保存到: {ARTIFACT_DIR}")

if __name__ == '__main__':
    main()
