""" Predictor Module - Fixed - Batch scoring - Unknown account warning flag - Model version hash check - No Streamlit """ import os import hashlib import joblib import xgboost as xgb import pandas as pd from src.config_loader import get_config _bundle_cache: dict = None _model_hash: str = None def _compute_model_hash(path: str) -> str: h = hashlib.md5() with open(path, 'rb') as f: h.update(f.read(65536)) return h.hexdigest() def load_model() -> dict: global _bundle_cache, _model_hash cfg = get_config()['ml'] model_path = cfg['model_path'] scaler_path = cfg['scaler_path'] if not os.path.exists(model_path): return None current_hash = _compute_model_hash(model_path) if _bundle_cache is not None and _model_hash == current_hash: return _bundle_cache model = xgb.XGBClassifier() model._estimator_type = 'classifier' model.load_model(model_path) meta = joblib.load(scaler_path) if os.path.exists(scaler_path) else {} feature_cols = meta.get('feature_cols', cfg['feature_cols']) _bundle_cache = { 'model': model, 'feature_cols': feature_cols, 'model_hash': current_hash, } _model_hash = current_hash return _bundle_cache def score_account(account_id: str, feature_df: pd.DataFrame, bundle: dict) -> dict: row = feature_df[feature_df['account'] == account_id] if row.empty: return {'risk_score': None, 'fraud_probability': None, 'unscored': True} cols = [c for c in bundle['feature_cols'] if c in row.columns] row = row.copy() row[cols] = row[cols].apply(pd.to_numeric, errors='coerce').fillna(0.0) proba = float(bundle['model'].predict_proba(row[cols])[0][1]) risk_score = int(proba * 99) return {'risk_score': risk_score, 'fraud_probability': proba, 'unscored': False} def score_accounts_batch(account_ids: list, feature_df: pd.DataFrame, bundle: dict) -> dict: """Vectorised batch scoring — fully numpy, no Python loops.""" rows = feature_df[feature_df['account'].isin(account_ids)] if rows.empty: return {aid: {'risk_score': None, 'fraud_probability': None, 'unscored': True} for aid in account_ids} cols = [c for c in bundle['feature_cols'] if c in rows.columns] rows = rows.copy() rows[cols] = rows[cols].apply(pd.to_numeric, errors='coerce').fillna(0.0) probas = bundle['model'].predict_proba(rows[cols])[:, 1] accounts = rows['account'].values risk_scores = (probas * 99).astype(int) result = { acct: {'risk_score': int(rs), 'fraud_probability': float(p), 'unscored': False} for acct, rs, p in zip(accounts, risk_scores, probas) } for aid in account_ids: if aid not in result: result[aid] = {'risk_score': None, 'fraud_probability': None, 'unscored': True} return result