From fdb8a5d0f0510a84875ceccfca876667f05c6134 Mon Sep 17 00:00:00 2001 From: Fahri Can Date: Tue, 5 May 2026 20:29:55 +0300 Subject: [PATCH] =?UTF-8?q?fix(ai-engine):=20sync=20FEATURE=5FCOLS=20with?= =?UTF-8?q?=20trained=20models=20(82=E2=86=92102=20features)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Load feature columns dynamically from feature_cols.json - Add 20 missing odds_*_present boolean flags to fallback list - Fixes LightGBM 'features in data (82) != training data (102)' crash --- ai-engine/models/v25_ensemble.py | 35 ++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/ai-engine/models/v25_ensemble.py b/ai-engine/models/v25_ensemble.py index 968745a..57bf161 100644 --- a/ai-engine/models/v25_ensemble.py +++ b/ai-engine/models/v25_ensemble.py @@ -141,8 +141,10 @@ class V25Predictor: Each market (MS, OU25, BTTS) has its own trained models. """ - # Feature columns (82 features, NO target leakage) - FEATURE_COLS = [ + # Feature columns — loaded dynamically from feature_cols.json to stay + # in sync with the trained models. The hardcoded list below is only a + # fallback in case the JSON file is missing. + _FALLBACK_FEATURE_COLS = [ # ELO Features (8) 'home_overall_elo', 'away_overall_elo', 'elo_diff', 'home_home_elo', 'away_away_elo', @@ -178,6 +180,17 @@ class V25Predictor: 'odds_ht_ou15_o', 'odds_ht_ou15_u', 'odds_btts_y', 'odds_btts_n', + # Odds Presence Flags (20) + 'odds_ms_h_present', 'odds_ms_d_present', 'odds_ms_a_present', + 'odds_ht_ms_h_present', 'odds_ht_ms_d_present', 'odds_ht_ms_a_present', + 'odds_ou05_o_present', 'odds_ou05_u_present', + 'odds_ou15_o_present', 'odds_ou15_u_present', + 'odds_ou25_o_present', 'odds_ou25_u_present', + 'odds_ou35_o_present', 'odds_ou35_u_present', + 'odds_ht_ou05_o_present', 'odds_ht_ou05_u_present', + 'odds_ht_ou15_o_present', 'odds_ht_ou15_u_present', + 'odds_btts_y_present', 'odds_btts_n_present', + # League Features (4) 'home_xga', 'away_xga', 'league_avg_goals', 'league_zero_goal_rate', @@ -198,6 +211,24 @@ class V25Predictor: 'home_missing_impact', 'away_missing_impact', 'home_goals_form', 'away_goals_form', ] + + @staticmethod + def _load_feature_cols() -> list: + """Load feature columns from feature_cols.json, falling back to hardcoded list.""" + feature_json = os.path.join(MODELS_DIR, 'feature_cols.json') + try: + if os.path.exists(feature_json): + with open(feature_json, 'r', encoding='utf-8') as f: + cols = json.load(f) + if isinstance(cols, list) and len(cols) > 0: + print(f"[V25] Loaded {len(cols)} feature columns from feature_cols.json") + return cols + except Exception as e: + print(f"[V25] Warning: could not load feature_cols.json: {e}") + print(f"[V25] Using fallback feature columns ({len(V25Predictor._FALLBACK_FEATURE_COLS)} features)") + return V25Predictor._FALLBACK_FEATURE_COLS + + FEATURE_COLS = _load_feature_cols.__func__() # Model weights for ensemble DEFAULT_WEIGHTS = {