""" 🚀 PRODUCTION ML ENGINE v7.0 - COMPLETE AUTOML PIPELINE ======================================================== PRODUCTION-LEVEL Features: - 30+ ML Algorithms (Classification, Regression, Clustering) - Auto GPU/CPU Detection (CUDA, ROCm, Metal) - Advanced Data Cleaning (Smart Imputation, Outlier Detection) - Feature Selection (Variance, Correlation, Mutual Info, RFE) - Bayesian Hyperparameter Optimization (Optuna) - Ensemble Methods (Stacking, Voting, Blending) - NLP Pipeline (TF-IDF, Sentiment, Text Stats) - SMOTE Class Imbalance Handling - 20+ Visualization Charts """ import os import pickle import warnings from typing import Dict, List, Optional, Any, Tuple from dataclasses import dataclass from datetime import datetime import logging import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, RandomizedSearchCV, cross_val_score, StratifiedKFold, KFold from sklearn.preprocessing import LabelEncoder, StandardScaler, RobustScaler from sklearn.ensemble import StackingClassifier, StackingRegressor from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer, HashingVectorizer from sklearn.feature_selection import VarianceThreshold, SelectKBest, chi2, mutual_info_classif from sklearn.metrics import ( accuracy_score, precision_score, recall_score, f1_score, r2_score, mean_absolute_error, mean_squared_error, confusion_matrix, roc_auc_score ) # Global Cancellation Tracking CANCELLATION_FLAGS = {} class TrainingCancelledError(Exception): """Exception raised when training is cancelled by user""" pass def cancel_training(user_id: str): """Signal training to stop for a user""" CANCELLATION_FLAGS[user_id] = True logging.info(f"🚫 Stop signal received for user {user_id}") def check_cancellation(user_id: str): """Check if training should stop, raise exception if so""" if CANCELLATION_FLAGS.get(user_id, False): # Clear flag and raise error CANCELLATION_FLAGS[user_id] = False logging.info(f"🚫 Aborting training for user {user_id}") raise TrainingCancelledError("Training cancelled by user") # Global Cancellation Tracking CANCELLATION_FLAGS = {} class TrainingCancelledError(Exception): """Exception raised when training is cancelled by user""" pass def cancel_training(user_id: str): """Signal training to stop for a user""" CANCELLATION_FLAGS[user_id] = True logging.info(f"🚫 Stop signal received for user {user_id}") def check_cancellation(user_id: str): """Check if training should stop, raise exception if so""" if CANCELLATION_FLAGS.get(user_id, False): # Clear flag and raise error CANCELLATION_FLAGS[user_id] = False logging.info(f"🚫 Aborting training for user {user_id}") raise TrainingCancelledError("Training cancelled by user") # Models - Classification & Regression from sklearn.ensemble import ( RandomForestClassifier, RandomForestRegressor, GradientBoostingClassifier, GradientBoostingRegressor, ExtraTreesClassifier, ExtraTreesRegressor, HistGradientBoostingClassifier, HistGradientBoostingRegressor, AdaBoostClassifier, AdaBoostRegressor, BaggingClassifier, BaggingRegressor ) from sklearn.linear_model import ( LogisticRegression, Ridge, ElasticNet, Lasso, SGDClassifier, SGDRegressor, PassiveAggressiveClassifier ) from sklearn.svm import SVC, SVR, LinearSVC, LinearSVR from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor from sklearn.naive_bayes import GaussianNB, MultinomialNB from sklearn.neural_network import MLPClassifier, MLPRegressor # IMPORT NEW PRODUCTION PIPELINE CLASSES try: from ml.production_ml_core import ( ProductionDataCleaner, ProductionFeatureEngineer, ProductionModelTrainer ) except ImportError: # Fallback or local definition if module not found (should not happen in prod) logging.warning("Could not import production_ml_core, using local fallback") from sklearn.ensemble import IsolationForest # Outlier detection from sklearn.naive_bayes import MultinomialNB, GaussianNB, ComplementNB from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor from sklearn.neural_network import MLPClassifier, MLPRegressor from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor try: from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering, SpectralClustering, MeanShift from sklearn.mixture import GaussianMixture except ImportError: pass from sklearn.decomposition import PCA, LatentDirichletAllocation from sklearn.metrics import silhouette_score from sklearn.discriminant_analysis import LinearDiscriminantAnalysis, QuadraticDiscriminantAnalysis from sklearn.linear_model import ( LogisticRegression, Ridge, ElasticNet, Lasso, SGDClassifier, SGDRegressor, PassiveAggressiveClassifier, BayesianRidge, HuberRegressor, PoissonRegressor, QuantileRegressor, LassoLars, OrthogonalMatchingPursuit ) from sklearn.naive_bayes import MultinomialNB, GaussianNB, ComplementNB, BernoulliNB from sklearn.preprocessing import PowerTransformer, QuantileTransformer try: import xgboost as xgb HAS_XGB = True except ImportError: HAS_XGB = False try: import lightgbm as lgb HAS_LGB = True except ImportError: HAS_LGB = False try: from catboost import CatBoostClassifier, CatBoostRegressor HAS_CATBOOST = True except ImportError: HAS_CATBOOST = False # Optuna for Bayesian hyperparameter optimization try: import optuna optuna.logging.set_verbosity(optuna.logging.WARNING) HAS_OPTUNA = True except ImportError: HAS_OPTUNA = False # SMOTE for class imbalance handling try: from imblearn.over_sampling import SMOTE from imblearn.under_sampling import RandomUnderSampler HAS_IMBLEARN = True except ImportError: HAS_IMBLEARN = False from ml.feature_engineer import AdvancedFeatureEngineer # Import Production ML Core (AutoML Pipeline) try: from ml.production_ml_core import ( ProductionDataCleaner, ProductionFeatureEngineer, ProductionModelTrainer, production_train_pipeline ) HAS_PRODUCTION_ML = True except ImportError: HAS_PRODUCTION_ML = False warnings.filterwarnings('ignore') logger = logging.getLogger(__name__) STORAGE_PATH = "./storage/automl" # ============================================================================= # GPU/CPU AUTO-DETECTION # ============================================================================= def detect_compute_device() -> Dict[str, Any]: """Auto-detect available compute device (GPU/CPU)""" device_info = {'device': 'cpu', 'gpu_available': False, 'gpu_name': None} # Check CUDA (NVIDIA) try: import torch if torch.cuda.is_available(): device_info['device'] = 'cuda' device_info['gpu_available'] = True device_info['gpu_name'] = torch.cuda.get_device_name(0) logger.info(f"🚀 GPU Detected: {device_info['gpu_name']}") return device_info except ImportError: pass # Check XGBoost GPU if HAS_XGB: try: import xgboost as xgb # XGBoost will use GPU if available via 'device' param device_info['xgb_gpu'] = True except: device_info['xgb_gpu'] = False logger.info("💻 Using CPU for training") return device_info DEVICE_INFO = detect_compute_device() @dataclass class TrainResult: """Training result with charts and metrics""" success: bool task_type: str target_column: str feature_columns: List[str] best_model_name: str best_model_metrics: Dict[str, float] leaderboard: List[Dict] feature_importance: List[Dict] y_test: np.ndarray y_pred: np.ndarray y_proba: Optional[np.ndarray] feature_metadata: List[Dict] n_rows: int n_cols: int processing_time: float charts: Optional[Dict[str, str]] = None # NEW: Base64 encoded charts is_nlp_task: bool = False # NEW: Flag for NLP tasks primary_text_col: Optional[str] = None # NEW: Primary text column for NLP cleaned_file_path: Optional[str] = None # NEW: Path to cleaned dataset reliability_score: Optional[float] = None # 🛡️ PRODUCTION INTELLIGENCE: Model reliability (0-100) validation_warnings: Optional[List[str]] = None # 🛡️ Any warnings from production validation class ProductionMLEngine: """ 🚀 PRODUCTION ML ENGINE v6.0 - ADAPTIVE INTELLIGENT AUTOML ========================================================== PRODUCTION-LEVEL Features: - Adaptive technique selection based on data profile - Automatic algorithm selection (not hardcoded) - Dynamic preprocessing based on data characteristics - NLP pipeline with 10+ text techniques - 20+ ML algorithms competing - Intelligent model selection based on data size/type - Cross-validation with optimal fold selection - Early stopping for efficiency """ def __init__(self): # Model self.model = None self.feature_columns = [] self.target_col = None self.task_type = "classification" self.task_type_simple = "classification" # classification/regression self.n_classes = None # Fix: Initialize for regression tasks self.label_encoders = {} self.scaler = None self.metrics = {} self.is_nlp_task = False self.primary_text_col = None # Advanced Feature Engineering self.feature_engineer = AdvancedFeatureEngineer() # NLP Components self.tfidf_vectorizer = None self.count_vectorizer = None self.nlp_feature_names = [] # Metadata self.feature_metadata = [] # List of dicts with type info self.imputer_vals = {} # Optimization self.best_params = {} self.cv_results = {} # Data Profile self.data_profile = { 'n_samples': 0, 'n_features': 0, 'n_numeric': 0, 'n_categorical': 0, 'n_text': 0, 'is_high_dimensional': False, 'is_small_data': False, 'is_large_data': False, 'is_imbalanced': False, 'is_nlp_task': False, 'has_missing': False, 'missing_ratio': 0.0, 'recommended_algorithms': [], 'recommended_techniques': [] } # Columns self.target_column: str = "" self.feature_columns: List[str] = [] self.numeric_cols: List[str] = [] self.categorical_cols: List[str] = [] self.text_cols: List[str] = [] self.dropped_cols: List[str] = [] # Preprocessing self.label_encoders: Dict[str, LabelEncoder] = {} self.target_encoder: Optional[LabelEncoder] = None self.text_vectorizers: Dict[str, TfidfVectorizer] = {} self.scaler: Optional[RobustScaler] = None self.numeric_fill_values: Dict[str, float] = {} self.feature_metadata: List[Dict] = [] # Stored test data for predict mode access self._y_test = None self._y_pred = None self._y_proba = None self._confusion_matrix = None self._training_data = None # Store sample of training data @property def feature_importance(self) -> Dict[str, float]: """Get feature importance as a dictionary {feature_name: importance_value}""" if self.model is None: return {} importance_list = self._get_importance(self.model) if not importance_list: return {} # Convert list of dicts to dictionary return {item['feature']: item['importance'] for item in importance_list} def get_model_metrics(self) -> Dict[str, Any]: """ Get comprehensive model metrics for predict mode. Returns stored y_test, y_pred, confusion_matrix, and all metrics. """ result = { 'model_name': self.model_name if hasattr(self, 'model_name') else 'Unknown', 'task_type': self.task_type, 'target': self.target_column, 'n_features': len(self.feature_columns) if self.feature_columns else 0, 'features': self.feature_columns[:20] if self.feature_columns else [], 'metrics': self.metrics if hasattr(self, 'metrics') else {}, 'n_classes': self.n_classes, } # Add stored test data for charts if hasattr(self, '_y_test') and self._y_test is not None: result['y_test'] = self._y_test.tolist() if hasattr(self._y_test, 'tolist') else list(self._y_test) if hasattr(self, '_y_pred') and self._y_pred is not None: result['y_pred'] = self._y_pred.tolist() if hasattr(self._y_pred, 'tolist') else list(self._y_pred) if hasattr(self, '_y_proba') and self._y_proba is not None: result['y_proba'] = self._y_proba.tolist() if hasattr(self._y_proba, 'tolist') else list(self._y_proba) if hasattr(self, '_confusion_matrix') and self._confusion_matrix is not None: result['confusion_matrix'] = self._confusion_matrix.tolist() if hasattr(self._confusion_matrix, 'tolist') else self._confusion_matrix # Add class labels if hasattr(self, 'target_encoder') and self.target_encoder is not None: result['class_names'] = self.target_encoder.classes_.tolist() # Add feature metadata for distribution charts if hasattr(self, 'feature_metadata') and self.feature_metadata: result['feature_metadata'] = self.feature_metadata # Add data profile if hasattr(self, 'data_profile') and self.data_profile: result['data_profile'] = self.data_profile return result def get_error_distribution(self) -> Dict[str, Any]: """ Get error distribution data for regression tasks. Returns residuals, MAE, RMSE, etc. """ if not hasattr(self, '_y_test') or self._y_test is None: return {'available': False, 'reason': 'No test data stored'} if not hasattr(self, '_y_pred') or self._y_pred is None: return {'available': False, 'reason': 'No predictions stored'} try: y_test = np.array(self._y_test) y_pred = np.array(self._y_pred) # Calculate residuals residuals = y_test - y_pred result = { 'available': True, 'residuals': residuals.tolist(), 'y_test': y_test.tolist(), 'y_pred': y_pred.tolist(), 'mae': float(np.mean(np.abs(residuals))), 'rmse': float(np.sqrt(np.mean(residuals**2))), 'std': float(np.std(residuals)), 'min_error': float(np.min(residuals)), 'max_error': float(np.max(residuals)), 'mean_error': float(np.mean(residuals)), 'median_error': float(np.median(residuals)), } return result except Exception as e: return {'available': False, 'reason': str(e)} def get_data_statistics(self, column_name: str = None) -> Dict[str, Any]: """ Get statistics for a specific column or all columns. Used by predict mode for data analysis. """ result = {} if hasattr(self, '_training_data') and self._training_data is not None: df = self._training_data if column_name and column_name in df.columns: col = df[column_name] if pd.api.types.is_numeric_dtype(col): result = { 'column': column_name, 'type': 'numeric', 'count': int(col.count()), 'mean': float(col.mean()), 'std': float(col.std()), 'min': float(col.min()), 'max': float(col.max()), 'median': float(col.median()), 'q25': float(col.quantile(0.25)), 'q75': float(col.quantile(0.75)), } else: result = { 'column': column_name, 'type': 'categorical', 'count': int(col.count()), 'unique': int(col.nunique()), 'top': str(col.mode().iloc[0]) if not col.mode().empty else None, 'frequency': int(col.value_counts().iloc[0]) if not col.value_counts().empty else 0, } else: # Return summary for all columns result = { 'n_rows': len(df), 'n_columns': len(df.columns), 'columns': list(df.columns[:20]), 'numeric_columns': list(df.select_dtypes(include=[np.number]).columns), 'categorical_columns': list(df.select_dtypes(include=['object']).columns), } return result # ========================================================================= # COLUMN ANALYSIS & SELECTION # ========================================================================= def _analyze_column(self, series: pd.Series, col_name: str) -> Dict: """Analyze a single column and determine its type and usefulness - ENHANCED""" n_unique = series.nunique() n_total = len(series) n_missing = series.isna().sum() missing_pct = n_missing / n_total unique_ratio = n_unique / n_total if n_total > 0 else 0 # Determine column type col_lower = col_name.lower() # Check if it's an ID column (expanded patterns) # Check if it's an ID column (expanded patterns) id_patterns = ['id', '_id', 'index', 'guid', 'uuid', 'pk', 'fk'] # SAFEGUARD: Don't treat money/metrics as ID (e.g. 'rate_id' might be bad but 'hourly_rate' is good) important_keywords = ['rate', 'usd', 'eur', 'price', 'cost', 'income', 'salary', 'revenue', 'amount', 'score', 'percent'] is_important = any(x in col_lower for x in important_keywords) if is_important: is_id = False else: # Only treat as ID if it strictly matches patterns AND is highly unique is_id = any(pat in col_lower for pat in id_patterns) and unique_ratio > 0.95 # Check if it's a date column # Check if it's a date column - RESTRICTED to prevent dropping 'years_experience' date_patterns = ['date', 'time', 'timestamp', 'created', 'updated', 'modified', 'datetime', 'dt'] # Only check year/month/day if they are EXACT matches or suffix, not just 'contains' if any(x in col_lower for x in date_patterns): is_date = True elif col_lower in ['year', 'month', 'day']: is_date = True else: is_date = False # Check if it's metadata/url column (useless for prediction) # BUT only if the content is short - long text should be kept for NLP # Check if it's metadata/url column (useless for prediction) # BUT only if the content is short - long text should be kept for NLP useless_patterns = ['url', 'link', 'href', 'path', 'file', 'image', 'photo', 'pic', 'ip', 'hash', 'password', 'token'] # Removed 'address', 'phone' as they might be useful features in some contexts # Removed vague terms to prevent false positives # Check average content length for text columns avg_content_len = 0 if series.dtype == 'object' or series.dtype == 'string': avg_content_len = series.astype(str).str.len().mean() # Only mark as useless if content is short (not real text for NLP) is_useless = any(x in col_lower for x in useless_patterns) and avg_content_len < 50 # Special case: "email" in name but long content = likely email body text, keep it if 'email' in col_lower and avg_content_len > 30: is_useless = False # This is email content, not email address # Check if numeric is_numeric = pd.api.types.is_numeric_dtype(series) # Check if boolean is_bool = series.dtype == bool or (is_numeric and n_unique == 2 and set(series.dropna().unique()).issubset({0, 1})) # Determine usefulness should_drop = False drop_reason = None if is_id: should_drop = True drop_reason = "ID column (unique identifier)" elif is_date: should_drop = True drop_reason = "Date column (requires special handling)" elif is_useless: should_drop = True drop_reason = "Metadata/URL column (not predictive)" elif missing_pct > 0.5: # Lowered from 0.7 to 0.5 should_drop = True drop_reason = f"Too many missing ({missing_pct:.0%})" elif n_unique == 1: should_drop = True drop_reason = "Constant value" elif not is_numeric and unique_ratio > 0.9: # Lowered from 0.95 should_drop = True drop_reason = "Near-unique per row (no pattern)" elif n_unique == n_total and is_numeric: # Check if purely sequential (1,2,3,4...) try: vals = series.dropna().values if len(vals) > 10: diffs = np.diff(np.sort(vals)) if np.std(diffs) < 0.01: should_drop = True drop_reason = "Sequential numeric (likely index)" except: pass # Determine final type if is_numeric: if is_bool: dtype = 'boolean' else: dtype = 'numeric' elif n_unique <= 50: dtype = 'categorical' else: dtype = 'text' return { 'name': col_name, 'dtype': dtype, 'n_unique': n_unique, 'n_missing': n_missing, 'missing_pct': missing_pct, 'unique_ratio': unique_ratio, 'should_drop': should_drop, 'drop_reason': drop_reason } def _select_columns(self, df: pd.DataFrame, target_col: str) -> Tuple[List[str], List[str], List[str], List[str]]: """ Smart column selection: - Keep useful numeric columns - Keep categorical with reasonable cardinality - Keep text columns for NLP (even if unique - every email IS unique!) - Drop IDs, dates, constants, and mostly-missing columns """ numeric_cols = [] categorical_cols = [] text_cols = [] dropped = [] print("📊 Analyzing columns...") logger.info("📊 Analyzing columns...") # First pass: detect if this is an NLP dataset is_nlp = getattr(self, 'is_nlp_task', False) primary_text = getattr(self, 'primary_text_col', None) for col in df.columns: if col == target_col: continue analysis = self._analyze_column(df[col], col) # SPECIAL CASE: For NLP datasets, ALWAYS keep text columns if analysis['dtype'] == 'text' and (is_nlp or col == primary_text): text_cols.append(col) logger.info(f" ✅ {col}: NLP text column (kept for text classification)") continue if analysis['should_drop']: dropped.append(col) logger.info(f" ❌ {col}: {analysis['drop_reason']}") elif analysis['dtype'] == 'numeric' or analysis['dtype'] == 'boolean': numeric_cols.append(col) elif analysis['dtype'] == 'categorical': categorical_cols.append(col) elif analysis['dtype'] == 'text': # For non-NLP datasets, still keep text if avg length is long enough avg_len = df[col].astype(str).str.len().mean() if avg_len > 30: # Long text = valuable content text_cols.append(col) logger.info(f" ✅ {col}: text column (avg {avg_len:.0f} chars)") elif analysis['unique_ratio'] < 0.9: text_cols.append(col) else: dropped.append(col) logger.info(f" ❌ {col}: Short unique text (no pattern)") print(f" ✅ Keeping: {len(numeric_cols)} numeric, {len(categorical_cols)} categorical, {len(text_cols)} text") print(f" ❌ Dropped: {len(dropped)} columns") return numeric_cols, categorical_cols, text_cols, dropped return numeric_cols, categorical_cols, text_cols, dropped def _calculate_feature_metadata(self, df: pd.DataFrame) -> List[Dict]: """ Calculate metadata (min, max, mean, type) for RAW columns. This allows the Frontend to show correct placeholders (e.g. 50,000 instead of 50). Now includes datetime columns for date picker UI support. IMPORTANT: Auto-detects column types from DataFrame if self.numeric_cols etc. are empty. """ metadata = [] # Get target column to exclude it target_col = getattr(self, 'target_column', None) or getattr(self, 'target_col', None) # Auto-detect column types if not already set numeric_cols = list(self.numeric_cols) if hasattr(self, 'numeric_cols') and self.numeric_cols else [] categorical_cols = list(self.categorical_cols) if hasattr(self, 'categorical_cols') and self.categorical_cols else [] text_cols = list(self.text_cols) if hasattr(self, 'text_cols') and self.text_cols else [] datetime_cols = [] # Date patterns for detection date_patterns = ['date', 'time', 'timestamp', 'created', 'updated', 'modified', 'datetime', 'dt'] # ID patterns to skip id_patterns = ['id', '_id', 'index', 'guid', 'uuid', 'pk', 'fk', 'unnamed'] # If column lists are empty, auto-detect from DataFrame if not numeric_cols and not categorical_cols and not text_cols: logger.info("[feature_metadata] Auto-detecting column types from DataFrame") for col in df.columns: # Skip target column if col == target_col: continue col_lower = col.lower() # Skip ID columns if any(pat in col_lower for pat in id_patterns): continue # Check if datetime is_datetime = False if pd.api.types.is_datetime64_any_dtype(df[col]): is_datetime = True elif any(x in col_lower for x in date_patterns): # Verify it's actually a date by trying to parse try: sample = df[col].dropna().head(5) if len(sample) > 0: pd.to_datetime(sample.iloc[0]) is_datetime = True except: pass if is_datetime: datetime_cols.append(col) continue # Check if numeric if pd.api.types.is_numeric_dtype(df[col]): numeric_cols.append(col) continue # Check if categorical vs text (based on unique values and avg length) try: n_unique = df[col].nunique() avg_len = df[col].astype(str).str.len().mean() if n_unique <= 50 and avg_len < 50: categorical_cols.append(col) else: text_cols.append(col) except: categorical_cols.append(col) else: # Use existing column lists, but still detect datetime columns for col in df.columns: if col == target_col: continue if col in numeric_cols or col in categorical_cols or col in text_cols: continue col_lower = col.lower() is_datetime = False # Check by column name pattern if any(x in col_lower for x in date_patterns): is_datetime = True elif col_lower in ['year', 'month', 'day']: is_datetime = True # Check by dtype if pd.api.types.is_datetime64_any_dtype(df[col]): is_datetime = True # Check by parsing attempt if not is_datetime and df[col].dtype == 'object': try: sample = df[col].dropna().head(5) if len(sample) > 0: pd.to_datetime(sample.iloc[0]) is_datetime = True except: pass if is_datetime: datetime_cols.append(col) logger.info(f"[feature_metadata] Detected: {len(numeric_cols)} numeric, {len(categorical_cols)} categorical, {len(text_cols)} text, {len(datetime_cols)} datetime") # Add numeric features for col in numeric_cols: if col in df.columns: try: metadata.append({ 'name': col, 'type': 'numeric', 'min': float(df[col].min()), 'max': float(df[col].max()), 'mean': float(df[col].mean()) }) except: pass # Add categorical features for col in categorical_cols: if col in df.columns: try: # Limit options to top 10 for UI performance options = df[col].value_counts().nlargest(10).index.tolist() metadata.append({ 'name': col, 'type': 'categorical', 'options': [str(x) for x in options] }) except: pass # Add text features for col in text_cols: if col in df.columns: metadata.append({ 'name': col, 'type': 'text', 'placeholder': f'Enter {col}...' }) # Add datetime features for date picker UI for col in datetime_cols: if col in df.columns: try: dt_series = pd.to_datetime(df[col], errors='coerce') min_date = dt_series.min() max_date = dt_series.max() metadata.append({ 'name': col, 'type': 'datetime', 'min': min_date.isoformat() if pd.notna(min_date) else None, 'max': max_date.isoformat() if pd.notna(max_date) else None, 'placeholder': 'Select date...' }) except Exception as e: # Fallback: add as text input metadata.append({ 'name': col, 'type': 'datetime', 'placeholder': 'Enter date (YYYY-MM-DD)...' }) self.feature_metadata = metadata logger.info(f"[feature_metadata] Total features for UI: {len(metadata)}") return metadata # ========================================================================= # TARGET DETECTION # ========================================================================= def _detect_target(self, df: pd.DataFrame) -> str: """Smart target detection with priority keywords""" # Priority 1: Exact match keywords exact_keywords = ['target', 'label', 'class', 'output', 'y'] for col in df.columns: if col.lower().strip() in exact_keywords: logger.info(f"🎯 Target (exact match): {col}") return col # Priority 2: Contains keywords (sorted by priority) contains_keywords = [ 'target', 'label', 'class', 'churn', 'fraud', 'default', 'outcome', 'survived', 'sentiment', 'rating', 'score', 'price', 'salary', 'income', 'revenue', 'amount', 'value', 'total' ] for keyword in contains_keywords: for col in df.columns: if keyword in col.lower(): logger.info(f"🎯 Target (contains '{keyword}'): {col}") return col # Priority 3: Heuristic - prefer last column if it's a good target last_col = df.columns[-1] last_unique = df[last_col].nunique() if last_unique <= 20 or pd.api.types.is_numeric_dtype(df[last_col]): logger.info(f"🎯 Target (last column): {last_col}") return last_col # Priority 4: Find best candidate candidates = [] for col in df.columns: n_unique = df[col].nunique() is_numeric = pd.api.types.is_numeric_dtype(df[col]) score = 0 if n_unique == 2: score = 100 # Binary - great target elif n_unique <= 10: score = 50 # Multiclass elif is_numeric: score = 30 # Regression if score > 0: candidates.append((col, score)) if candidates: candidates.sort(key=lambda x: x[1], reverse=True) logger.info(f"🎯 Target (best candidate): {candidates[0][0]}") return candidates[0][0] logger.info(f"🎯 Target (fallback): {df.columns[-1]}") return df.columns[-1] def _detect_task_type(self, y: pd.Series) -> Tuple[str, int]: """ PRODUCTION-LEVEL Task Type Detection """ n_unique = y.nunique() n_samples = len(y) unique_ratio = n_unique / n_samples if n_samples > 0 else 0 logger.info(f" 🔍 Analyzing target: {n_unique} unique values, ratio={unique_ratio:.2%}") # ====== RULE 0: PRIORITY - Check if convertible to Numeric first! ====== try: y_numeric = pd.to_numeric(y.astype(str).str.replace(r'[$,]', '', regex=True), errors='coerce') if y_numeric.notna().sum() > len(y) * 0.9: # 90% valid numbers y = y_numeric n_unique = y.nunique() logger.info(" ✅ Converted target to numeric (was string/mixed)") except: pass # ====== RULE 1: String/Object type = Classification (if NOT numeric) ====== if pd.api.types.is_object_dtype(y) or pd.api.types.is_categorical_dtype(y): if n_unique == 2: logger.info(f" ✅ Binary Classification (string, 2 classes)") return 'binary_classification', 2 elif n_unique <= 50: logger.info(f" ✅ Multiclass Classification (string, {n_unique} classes)") return 'multiclass_classification', n_unique else: avg_len = y.astype(str).str.len().mean() if avg_len > 20: pass logger.warning(f" ⚠️ High-cardinality string ({n_unique} unique) - treating as multiclass") return 'multiclass_classification', n_unique # ====== RULE 2: Boolean = Binary Classification ====== if pd.api.types.is_bool_dtype(y): logger.info(f" ✅ Binary Classification (boolean)") return 'binary_classification', 2 # ====== RULE 3: Numeric Analysis ====== y_clean = pd.to_numeric(y, errors='coerce').dropna() if len(y_clean) == 0: logger.warning(f" ⚠️ No valid numeric values - defaulting to regression") return 'regression', 0 # Check for binary (0/1 or similar) unique_vals = set(y_clean.unique()) if unique_vals.issubset({0, 1}) or unique_vals.issubset({-1, 1}): logger.info(f" ✅ Binary Classification (0/1 or -1/1)") return 'binary_classification', 2 # Check if values are whole numbers is_whole_numbers = (y_clean % 1 == 0).all() # Integer with few unique values = Classification if is_whole_numbers: if n_unique == 2: logger.info(f" ✅ Binary Classification (2 integer classes)") return 'binary_classification', 2 elif n_unique <= 10: logger.info(f" ✅ Multiclass Classification ({n_unique} integer classes)") return 'multiclass_classification', n_unique elif n_unique <= 20 and unique_ratio < 0.05: # Low unique ratio suggests classification logger.info(f" ✅ Multiclass Classification ({n_unique} classes, low ratio)") return 'multiclass_classification', n_unique # ====== RULE 4: Continuous values = Regression ====== # Check for decimal places has_decimals = not is_whole_numbers # Check value range (large range suggests regression) val_range = y_clean.max() - y_clean.min() if has_decimals: logger.info(f" ✅ Regression (continuous decimals, range={val_range:.2f})") return 'regression', 0 if unique_ratio > 0.1 and n_unique > 20: logger.info(f" ✅ Regression (high unique ratio={unique_ratio:.1%}, {n_unique} values)") return 'regression', 0 if val_range > 100 and n_unique > 30: logger.info(f" ✅ Regression (large range={val_range:.0f})") return 'regression', 0 # Default based on unique count if n_unique <= 15: logger.info(f" ✅ Multiclass Classification ({n_unique} classes, default)") return 'multiclass_classification', n_unique logger.info(f" ✅ Regression (default, {n_unique} unique values)") return 'regression', 0 # ========================================================================= # DATA CLEANING & PREPROCESSING # ========================================================================= def _clean_numeric(self, series: pd.Series) -> Tuple[np.ndarray, float]: """Clean numeric column: handle missing, outliers""" # Convert to numeric, coerce errors numeric = pd.to_numeric(series, errors='coerce') # Calculate fill value (median is robust to outliers) fill_val = numeric.median() if pd.isna(fill_val): fill_val = 0.0 # Fill missing cleaned = numeric.fillna(fill_val).values.astype(float) # Handle inf cleaned = np.nan_to_num(cleaned, nan=fill_val, posinf=fill_val, neginf=fill_val) return cleaned, fill_val def _cap_outliers(self, data: np.ndarray, method: str = 'hybrid') -> np.ndarray: """ PRODUCTION-LEVEL Outlier Handling Methods: - 'iqr': Traditional IQR method (fast) - 'isolation': Isolation Forest (ML-based) - 'hybrid': Both methods combined (recommended) """ if len(data) < 10: return data try: outlier_mask = np.zeros(len(data), dtype=bool) # Method 1: IQR-based detection q1 = np.percentile(data, 25) q3 = np.percentile(data, 75) iqr = q3 - q1 lower_bound = q1 - 1.5 * iqr upper_bound = q3 + 1.5 * iqr iqr_outliers = (data < lower_bound) | (data > upper_bound) outlier_mask |= iqr_outliers # Method 2: Isolation Forest (for complex patterns) if method in ['isolation', 'hybrid'] and len(data) > 50: try: iso_forest = IsolationForest(contamination=0.05, random_state=42, n_jobs=-1) preds = iso_forest.fit_predict(data.reshape(-1, 1)) iso_outliers = preds == -1 outlier_mask |= iso_outliers except: pass # Winsorize: clip to 1st/99th percentile p1, p99 = np.percentile(data, [1, 99]) capped = np.clip(data, p1, p99) n_capped = np.sum(outlier_mask) if n_capped > 0: logger.info(f" 🛠️ Detected {n_capped} outliers ({n_capped/len(data)*100:.1f}%)") return capped except: return data def _remove_highly_correlated(self, df: pd.DataFrame, threshold: float = 0.95) -> List[str]: """Remove features with very high correlation (likely duplicates or leakage)""" numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() if len(numeric_cols) < 2: return [] try: corr_matrix = df[numeric_cols].corr().abs() upper_triangle = corr_matrix.where( np.triu(np.ones(corr_matrix.shape), k=1).astype(bool) ) to_drop = [col for col in upper_triangle.columns if any(upper_triangle[col] > threshold)] if to_drop: logger.warning(f" ⚠️ Removing {len(to_drop)} highly correlated features (>{threshold:.0%} correlation)") for col in to_drop: logger.warning(f" - {col}") return to_drop except: return [] def _production_feature_selection(self, X: np.ndarray, y: np.ndarray, feature_names: List[str]) -> Tuple[np.ndarray, List[str], Dict]: """ PRODUCTION-LEVEL Feature Selection Pipeline """ logger.info(" 🔍 FEATURE SELECTION PIPELINE...") selection_info = {'original_features': len(feature_names), 'steps': []} current_X = X.copy() current_features = list(feature_names) # Step 1: Variance Threshold (remove zero variance) try: from sklearn.feature_selection import VarianceThreshold selector = VarianceThreshold(threshold=0.01) mask = selector.fit(current_X).get_support() current_X = current_X[:, mask] current_features = [f for f, m in zip(current_features, mask) if m] removed = len(feature_names) - len(current_features) if removed > 0: logger.info(f" ✂️ Variance filter: removed {removed} low-variance features") selection_info['steps'].append({'step': 'variance', 'removed': removed}) except: pass # Step 2: Correlation Filter (on remaining features) try: if current_X.shape[1] > 2: corr_matrix = np.corrcoef(current_X, rowvar=False) upper_tri = np.triu(np.abs(corr_matrix), k=1) high_corr_pairs = np.where(upper_tri > 0.95) cols_to_drop = set(high_corr_pairs[1]) # Drop second column of each pair mask = [i not in cols_to_drop for i in range(current_X.shape[1])] current_X = current_X[:, mask] current_features = [f for f, m in zip(current_features, mask) if m] if len(cols_to_drop) > 0: logger.info(f" ✂️ Correlation filter: removed {len(cols_to_drop)} correlated features") selection_info['steps'].append({'step': 'correlation', 'removed': len(cols_to_drop)}) except: pass # Step 3: Mutual Information (rank remaining features) try: if current_X.shape[1] > 10: from sklearn.feature_selection import mutual_info_classif, mutual_info_regression if self.task_type_simple == 'classification': mi_scores = mutual_info_classif(current_X, y, random_state=42) else: mi_scores = mutual_info_regression(current_X, y, random_state=42) # Keep top 80% by MI score or minimum 10 features k = max(10, int(len(current_features) * 0.8)) k = min(k, len(current_features)) top_indices = np.argsort(mi_scores)[-k:] current_X = current_X[:, top_indices] current_features = [current_features[i] for i in top_indices] removed = len(mi_scores) - k if removed > 0: logger.info(f" ✂️ MI ranking: kept top {k} informative features") selection_info['steps'].append({'step': 'mutual_info', 'kept': k}) except: pass selection_info['final_features'] = len(current_features) logger.info(f" ✅ Feature selection: {selection_info['original_features']} → {selection_info['final_features']}") return current_X, current_features, selection_info # ========================================================================= # DATA PROFILING - INTELLIGENT ADAPTIVE SELECTION # ========================================================================= def _analyze_data_profile(self, df: pd.DataFrame, target_col: str) -> Dict: """ PRODUCTION-LEVEL: Analyze data and recommend optimal techniques. This method profiles the data to intelligently select: - Best algorithms for this data size/type - Optimal preprocessing techniques - Appropriate model complexity """ profile = self.data_profile.copy() # Basic stats profile['n_samples'] = len(df) profile['n_features'] = len(df.columns) - 1 # Size categories profile['is_small_data'] = len(df) < 1000 profile['is_medium_data'] = 1000 <= len(df) < 50000 profile['is_large_data'] = len(df) >= 50000 # Dimensionality profile['is_high_dimensional'] = profile['n_features'] > 100 profile['is_low_dimensional'] = profile['n_features'] < 10 # Missing data missing_ratio = df.isna().sum().sum() / (len(df) * len(df.columns)) profile['has_missing'] = missing_ratio > 0.01 profile['missing_ratio'] = missing_ratio # Class imbalance for classification if target_col in df.columns: y = df[target_col] if y.dtype == 'object' or y.nunique() < 20: class_counts = y.value_counts() if len(class_counts) > 1: imbalance_ratio = class_counts.max() / class_counts.min() profile['is_imbalanced'] = imbalance_ratio > 3 profile['imbalance_ratio'] = imbalance_ratio # Count column types for col in df.columns: if col == target_col: continue if pd.api.types.is_numeric_dtype(df[col]): profile['n_numeric'] += 1 elif df[col].dtype == 'object' or df[col].dtype == 'string': avg_len = df[col].astype(str).str.len().mean() if avg_len > 20: # Lowered threshold to catch skills/titles profile['n_text'] += 1 else: profile['n_categorical'] += 1 # FIX: Activate NLP mode if ANY significant text column exists profile['is_nlp_task'] = profile['n_text'] > 0 # INTELLIGENT ALGORITHM RECOMMENDATIONS recommended = [] techniques = [] logger.info("📊 DATA PROFILE ANALYSIS:") logger.info(f" Samples: {profile['n_samples']:,} | Features: {profile['n_features']}") logger.info(f" Numeric: {profile['n_numeric']} | Categorical: {profile['n_categorical']} | Text: {profile['n_text']}") # === ALGORITHM SELECTION RULES === # Small data: prefer simpler models, avoid deep learning if profile['is_small_data']: recommended.extend(['LogisticRegression', 'RandomForest', 'SVM', 'KNN', 'DecisionTree', 'BayesianRidge', 'LDA']) techniques.append('cross_validation_10_fold') logger.info(" 📈 Small data → Simple models + Bayesian regression") # Medium data: balanced approach elif profile['is_medium_data']: recommended.extend(['RandomForest', 'XGBoost', 'LightGBM', 'HistGradientBoosting', 'MLP', 'HuberRegressor']) techniques.append('cross_validation_5_fold') logger.info(" 📈 Medium data → Ensemble + gradient boosting + robust regression") # Large data: gradient boosting shines, can use neural nets else: recommended.extend(['LightGBM', 'XGBoost', 'CatBoost', 'HistGradientBoosting', 'MLP', 'SGD']) techniques.append('cross_validation_3_fold') techniques.append('early_stopping') logger.info(" 📈 Large data → Fast gradient boosting + early stopping") # === SPECIALIZED RECOMMENDATIONS === # Count data (Regression): Positive integers imply Poisson if target_col in df.columns and pd.api.types.is_numeric_dtype(df[target_col]): y = df[target_col].dropna() if (y >= 0).all() and (y % 1 == 0).all() and y.max() > 1: recommended.append('PoissonRegressor') logger.info(" 🔢 Count data detected → Added PoissonRegressor") # High Dimensionality (p > n or large p) if profile['is_high_dimensional']: recommended.extend(['Lasso', 'ElasticNet', 'LinearSVC']) logger.info(" 🤏 High dimensionality → Added Regularized Linear Models") # High dimensional: regularization important if profile['is_high_dimensional']: recommended.extend(['SGDClassifier', 'LogisticRegression', 'LinearSVC']) techniques.append('feature_selection') techniques.append('regularization') logger.info(" 📈 High dimensional → Linear models + regularization") # NLP/Text data: specific algorithms if profile['is_nlp_task']: recommended = ['MultinomialNB', 'ComplementNB', 'LogisticRegression', 'LinearSVC', 'SGDClassifier', 'PassiveAggressive', 'MLP'] techniques.extend(['tfidf', 'ngrams', 'sentiment_features', 'text_stats']) logger.info(" 📈 NLP task → Text-optimized classifiers") # Imbalanced data: specific handling if profile.get('is_imbalanced', False): techniques.append('smote') techniques.append('class_weights') logger.info(f" ⚠️ Imbalanced (ratio: {profile.get('imbalance_ratio', 0):.1f}:1) → SMOTE + class weights") # Add ensemble techniques for accuracy techniques.append('stacking_ensemble') techniques.append('voting_ensemble') profile['recommended_algorithms'] = list(set(recommended)) profile['recommended_techniques'] = list(set(techniques)) logger.info(f" 🎯 Recommended: {', '.join(profile['recommended_algorithms'][:5])}") self.data_profile = profile return profile def _get_adaptive_models(self) -> Dict[str, Tuple[Any, Dict]]: """ PRODUCTION-LEVEL: Return models adapted to data profile. ANTI-OVERFITTING: Uses stronger regularization for smaller datasets. """ profile = self.data_profile recommended = profile.get('recommended_algorithms', []) n_samples = profile.get('n_samples', 1000) # ===== ANTI-OVERFITTING: Scale regularization based on dataset size ===== if n_samples < 500: # Very small dataset - strong regularization tree_max_depth = [3, 5, 7] tree_min_samples = [5, 10, 20] n_estimators = [30, 50, 75] C_values = [0.01, 0.1, 0.5] alpha_values = [1.0, 5.0, 10.0] mlp_alpha = [0.01, 0.05, 0.1] logger.info(f" 📐 Small dataset ({n_samples} samples): Using strong regularization") elif n_samples < 2000: # Medium dataset - moderate regularization tree_max_depth = [5, 8, 12] tree_min_samples = [3, 5, 10] n_estimators = [50, 100] C_values = [0.1, 0.5, 1.0] alpha_values = [0.1, 1.0, 5.0] mlp_alpha = [0.001, 0.01, 0.05] else: # Large dataset - lighter regularization tree_max_depth = [8, 12, 20] tree_min_samples = [2, 5] n_estimators = [100, 200] C_values = [0.5, 1.0, 5.0] alpha_values = [0.01, 0.1, 1.0] mlp_alpha = [0.0001, 0.001, 0.01] # === NLP SPECIFIC MODELS === if profile.get('is_nlp_task', False): logger.info(" 🔤 NLP Task Detected: Using specialized text classification models") # SPLIT: Check if regression or classification if self.task_type_simple == 'regression': logger.info(" Note: Regression task with text features - using robust regressors") models = { # Regularized Linear Models (good for high-dim text) 'Ridge': ( Ridge(random_state=42), {'alpha': alpha_values} ), 'ElasticNet': ( ElasticNet(random_state=42, max_iter=2000), {'alpha': [0.1, 0.5, 1.0], 'l1_ratio': [0.3, 0.5, 0.7]} ), 'BayesianRidge': ( BayesianRidge(), {'alpha_1': [1e-7, 1e-6, 1e-5], 'lambda_1': [1e-7, 1e-6, 1e-5]} ) } # Add XGBoost if available (excellent for any task) if HAS_XGB: models['XGBoost'] = ( xgb.XGBRegressor( n_estimators=n_estimators[0], max_depth=tree_max_depth[0], learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, reg_alpha=0.1, random_state=42, n_jobs=-1, verbosity=0 ), {'n_estimators': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) # Add LightGBM if available (fast and accurate) if HAS_LGB: models['LightGBM'] = ( lgb.LGBMRegressor( n_estimators=n_estimators[0], max_depth=tree_max_depth[0], learning_rate=0.1, reg_lambda=1.0, reg_alpha=0.1, min_child_samples=10, random_state=42, n_jobs=-1, verbose=-1 ), {'n_estimators': n_estimators, 'max_depth': tree_max_depth} ) # Gradient Boosting (sklearn - always available) models['GradientBoosting'] = ( GradientBoostingRegressor( n_estimators=n_estimators[0], max_depth=tree_max_depth[0], learning_rate=0.1, min_samples_leaf=tree_min_samples[0], random_state=42 ), {'n_estimators': n_estimators, 'max_depth': tree_max_depth} ) # Random Forest (robust, handles noise well) models['RandomForest'] = ( RandomForestRegressor( n_estimators=n_estimators[0], max_depth=tree_max_depth[-1], min_samples_leaf=tree_min_samples[0], random_state=42, n_jobs=-1 ), {'n_estimators': n_estimators, 'max_depth': tree_max_depth} ) return models else: return { 'MultinomialNB': (MultinomialNB(), {'alpha': [0.5, 1.0, 2.0]}), 'BernoulliNB': (BernoulliNB(), {'alpha': [0.5, 1.0, 2.0], 'binarize': [0.0, 0.5]}), 'ComplementNB': (ComplementNB(), {'alpha': [0.5, 1.0, 2.0]}), 'LogisticRegression': ( LogisticRegression(max_iter=2000, n_jobs=-1, solver='saga', penalty='l2'), {'C': C_values} ), 'SGDClassifier': ( SGDClassifier(max_iter=2000, n_jobs=-1, loss='hinge', penalty='l2'), {'alpha': [1e-3, 1e-2, 1e-1]} ), 'LinearSVC': ( LinearSVC(max_iter=2000), {'C': C_values} ), 'PassiveAggressive': ( PassiveAggressiveClassifier(max_iter=2000), {'C': C_values} ) } # All available models - with ANTI-OVERFITTING parameters all_classification_models = { 'LogisticRegression': ( LogisticRegression(max_iter=1000, random_state=42, n_jobs=-1), {'C': C_values, 'penalty': ['l1', 'l2'], 'solver': ['saga']} ), 'SGDClassifier': ( SGDClassifier(random_state=42, max_iter=1000, early_stopping=True), {'alpha': [0.001, 0.01, 0.1], 'penalty': ['l1', 'l2', 'elasticnet']} ), 'DecisionTree': ( DecisionTreeClassifier(random_state=42), {'max_depth': tree_max_depth, 'min_samples_split': tree_min_samples} ), 'RandomForest': ( RandomForestClassifier(n_jobs=-1, random_state=42, min_samples_leaf=tree_min_samples[0]), {'n_estimators': n_estimators, 'max_depth': tree_max_depth} ), 'ExtraTrees': ( ExtraTreesClassifier(n_jobs=-1, random_state=42, min_samples_leaf=tree_min_samples[0]), {'n_estimators': n_estimators, 'max_depth': tree_max_depth} ), 'HistGradientBoosting': ( HistGradientBoostingClassifier(random_state=42, early_stopping=True, min_samples_leaf=tree_min_samples[0]), {'max_iter': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ), 'AdaBoost': ( AdaBoostClassifier(random_state=42), {'n_estimators': n_estimators, 'learning_rate': [0.1, 0.5, 1.0]} ), 'SVM': ( SVC(random_state=42, probability=True), {'C': C_values, 'kernel': ['rbf', 'linear']} ), 'LinearSVC': ( LinearSVC(random_state=42, max_iter=2000, dual=False), {'C': C_values} ), 'GaussianNB': ( GaussianNB(), {'var_smoothing': [1e-9, 1e-8, 1e-7]} ), 'MultinomialNB': ( MultinomialNB(), {'alpha': [0.5, 1.0, 2.0]} ), 'ComplementNB': ( ComplementNB(), {'alpha': [0.5, 1.0, 2.0]} ), 'MLP': ( MLPClassifier(random_state=42, max_iter=500, early_stopping=True, validation_fraction=0.15), {'hidden_layer_sizes': [(64, 32), (100, 50)], 'alpha': mlp_alpha} ), 'KNN': ( KNeighborsClassifier(n_jobs=-1), {'n_neighbors': [3, 5, 7], 'weights': ['uniform', 'distance']} ), 'PassiveAggressive': ( PassiveAggressiveClassifier(random_state=42, max_iter=1000), {'C': C_values} ), } all_regression_models = { 'Ridge': (Ridge(random_state=42), {'alpha': alpha_values}), 'Lasso': (Lasso(random_state=42, max_iter=2000), {'alpha': [0.1, 0.5, 1.0, 5.0]}), 'ElasticNet': (ElasticNet(random_state=42, max_iter=2000), {'alpha': [0.1, 0.5, 1.0], 'l1_ratio': [0.2, 0.5, 0.8]}), 'SGDRegressor': (SGDRegressor(random_state=42, max_iter=1000, early_stopping=True), {'alpha': [0.001, 0.01, 0.1]}), 'DecisionTree': (DecisionTreeRegressor(random_state=42), {'max_depth': tree_max_depth, 'min_samples_split': tree_min_samples}), 'RandomForest': (RandomForestRegressor(n_jobs=-1, random_state=42, min_samples_leaf=tree_min_samples[0]), {'n_estimators': n_estimators, 'max_depth': tree_max_depth}), 'ExtraTrees': (ExtraTreesRegressor(n_jobs=-1, random_state=42, min_samples_leaf=tree_min_samples[0]), {'n_estimators': n_estimators, 'max_depth': tree_max_depth}), 'HistGradientBoosting': (HistGradientBoostingRegressor(random_state=42, early_stopping=True, min_samples_leaf=tree_min_samples[0]), {'max_iter': n_estimators, 'max_depth': tree_max_depth}), 'AdaBoost': (AdaBoostRegressor(random_state=42), {'n_estimators': n_estimators, 'learning_rate': [0.1, 0.5, 1.0]}), 'SVR': (SVR(), {'C': C_values, 'kernel': ['rbf', 'linear']}), 'MLP': (MLPRegressor(random_state=42, max_iter=500, early_stopping=True, validation_fraction=0.15), {'hidden_layer_sizes': [(64, 32), (100, 50)], 'alpha': mlp_alpha}), 'KNN': (KNeighborsRegressor(n_jobs=-1), {'n_neighbors': [3, 5, 7], 'weights': ['uniform', 'distance']}), } # Add gradient boosting libraries if available - with regularization if HAS_XGB: all_classification_models['XGBoost'] = ( xgb.XGBClassifier(n_jobs=-1, random_state=42, verbosity=0, eval_metric='logloss', reg_lambda=1.0, reg_alpha=0.1, min_child_weight=3), {'n_estimators': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) all_regression_models['XGBoost'] = ( xgb.XGBRegressor(n_jobs=-1, random_state=42, verbosity=0, reg_lambda=1.0, reg_alpha=0.1, min_child_weight=3), {'n_estimators': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) if HAS_LGB: all_classification_models['LightGBM'] = ( lgb.LGBMClassifier(n_jobs=-1, random_state=42, verbose=-1, reg_lambda=1.0, reg_alpha=0.1, min_child_samples=10), {'n_estimators': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) all_regression_models['LightGBM'] = ( lgb.LGBMRegressor(n_jobs=-1, random_state=42, verbose=-1, reg_lambda=1.0, reg_alpha=0.1, min_child_samples=10), {'n_estimators': n_estimators, 'max_depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) if HAS_CATBOOST: all_classification_models['CatBoost'] = ( CatBoostClassifier(random_state=42, verbose=0, thread_count=-1, l2_leaf_reg=3.0), {'iterations': n_estimators, 'depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) all_regression_models['CatBoost'] = ( CatBoostRegressor(random_state=42, verbose=0, thread_count=-1, l2_leaf_reg=3.0), {'iterations': n_estimators, 'depth': tree_max_depth, 'learning_rate': [0.05, 0.1]} ) # Select models based on task type all_models = all_classification_models if self.task_type_simple == 'classification' else all_regression_models # Filter to recommended models, but ensure we have at least 5 if recommended: selected = {k: v for k, v in all_models.items() if any(r.lower() in k.lower() for r in recommended)} # Add core models if we filtered too aggressively if len(selected) < 5: for key in ['RandomForest', 'HistGradientBoosting', 'LogisticRegression', 'Ridge']: if key in all_models and key not in selected: selected[key] = all_models[key] return selected return all_models # ========================================================================= # NLP PROCESSING PIPELINE - COMPREHENSIVE # ========================================================================= def _is_nlp_dataset(self, df: pd.DataFrame, target_col: str) -> Tuple[bool, str]: """Detect if this is primarily an NLP/text classification dataset""" text_cols = [] for col in df.columns: if col == target_col: continue series = df[col].dropna() if series.dtype == object or series.dtype == 'string': avg_len = series.astype(str).str.len().mean() avg_words = series.astype(str).str.split().str.len().mean() if avg_len > 20 or avg_words > 3: # Lowered threshold (was 50/5) text_cols.append(col) # If any text feature exists, we treat it as NLP-enhanced non_target_cols = [c for c in df.columns if c != target_col] # Relaxed logic: If we have ANY text column, return True if len(text_cols) > 0: return True, text_cols[0] if text_cols else None return False, None def _clean_text_nlp(self, text: str) -> str: """Advanced NLP text cleaning for better predictions""" import re if not isinstance(text, str) or not text: return "" # Convert to lowercase text = text.lower() # Remove URLs text = re.sub(r'http\S+|www\.\S+', ' ', text) # Remove HTML tags text = re.sub(r'<[^>]+>', ' ', text) # Remove email addresses text = re.sub(r'\S+@\S+', ' ', text) # Remove mentions and hashtags text = re.sub(r'[@#]\w+', ' ', text) # Remove numbers (keep words with numbers like "3d", "2nd") text = re.sub(r'\b\d+\b', ' ', text) # Remove special characters but keep apostrophes for contractions text = re.sub(r"[^a-z\s']", ' ', text) # Remove extra whitespace text = re.sub(r'\s+', ' ', text).strip() # Remove very short tokens (less than 2 chars) tokens = text.split() tokens = [t for t in tokens if len(t) > 1] return ' '.join(tokens) def _apply_stemming(self, text: str) -> str: """Apply Porter stemming for NLP""" try: from nltk.stem import PorterStemmer stemmer = PorterStemmer() tokens = text.split() return ' '.join([stemmer.stem(t) for t in tokens]) except: return text def _apply_lemmatization(self, text: str) -> str: """Apply WordNet lemmatization for NLP""" try: from nltk.stem import WordNetLemmatizer lemmatizer = WordNetLemmatizer() tokens = text.split() return ' '.join([lemmatizer.lemmatize(t) for t in tokens]) except: return text def _get_text_statistics(self, text: str) -> Dict[str, float]: """Extract statistical features from text""" if not text: return {'char_count': 0, 'word_count': 0, 'avg_word_len': 0, 'sentence_count': 0, 'uppercase_ratio': 0, 'digit_ratio': 0} words = text.split() sentences = text.count('.') + text.count('!') + text.count('?') + 1 return { 'char_count': len(text), 'word_count': len(words), 'avg_word_len': sum(len(w) for w in words) / max(len(words), 1), 'sentence_count': sentences, 'uppercase_ratio': sum(1 for c in text if c.isupper()) / max(len(text), 1), 'digit_ratio': sum(1 for c in text if c.isdigit()) / max(len(text), 1) } def _get_sentiment_features(self, text: str) -> Dict[str, float]: """Extract basic sentiment features without external dependencies""" # Positive and negative word lists (basic) positive_words = {'good', 'great', 'excellent', 'best', 'amazing', 'love', 'wonderful', 'fantastic', 'awesome', 'perfect', 'happy', 'positive', 'beautiful', 'nice', 'brilliant', 'superb', 'outstanding', 'recommend', 'enjoy', 'pleased', 'satisfied', 'delighted', 'impressive', 'helpful'} negative_words = {'bad', 'worst', 'terrible', 'awful', 'horrible', 'hate', 'poor', 'disappointing', 'useless', 'waste', 'negative', 'ugly', 'boring', 'broken', 'problem', 'issue', 'fail', 'failed', 'wrong', 'angry', 'frustrated', 'annoyed', 'disappointed', 'complaint', 'refund'} words = set(text.lower().split()) pos_count = len(words & positive_words) neg_count = len(words & negative_words) total = pos_count + neg_count return { 'positive_score': pos_count / max(len(words), 1), 'negative_score': neg_count / max(len(words), 1), 'sentiment_ratio': (pos_count - neg_count) / max(total, 1) if total > 0 else 0, 'polarity': 1 if pos_count > neg_count else (-1 if neg_count > pos_count else 0) } def _extract_nlp_features(self, text_series: pd.Series, col_name: str) -> np.ndarray: """ INDUSTRIAL-LEVEL NLP Feature Extraction with Dimensionality Reduction Key improvements: 1. TF-IDF + TruncatedSVD (LSA) for dense, meaningful features 2. Reduced dimensions to prevent overfitting 3. Text statistics for interpretability """ from sklearn.decomposition import TruncatedSVD from sklearn.pipeline import Pipeline logger.info(f" 🔤 NLP Processing: {col_name}") # Clean text clean_texts = text_series.fillna('').astype(str).apply(self._clean_text_nlp) logger.info(f" ✅ Text cleaned") n_samples = len(text_series) # 1. TF-IDF + SVD Pipeline (Latent Semantic Analysis) # Key: Reduce 200+ sparse features to 30-50 dense features try: n_components = min(50, n_samples // 3, 100) # Adaptive SVD components n_components = max(10, n_components) tfidf = TfidfVectorizer( max_features=500, # Capture more vocabulary stop_words='english', ngram_range=(1, 2), # Bigrams only (trigrams too sparse) min_df=2, max_df=0.85, lowercase=True, strip_accents='unicode', sublinear_tf=True ) # Apply TF-IDF first tfidf_matrix = tfidf.fit_transform(clean_texts) # Apply SVD if TF-IDF has enough features if tfidf_matrix.shape[1] > n_components: svd = TruncatedSVD(n_components=n_components, random_state=42) tfidf_features = svd.fit_transform(tfidf_matrix) self.text_svd_transformers = getattr(self, 'text_svd_transformers', {}) self.text_svd_transformers[col_name] = svd logger.info(f" ✅ TF-IDF+SVD: {n_components} dense features (from {tfidf_matrix.shape[1]} sparse)") else: tfidf_features = tfidf_matrix.toarray() logger.info(f" ✅ TF-IDF: {tfidf_features.shape[1]} features") self.text_vectorizers[col_name] = tfidf except Exception as e: logger.warning(f" ⚠️ TF-IDF failed: {e}") tfidf_features = np.zeros((len(text_series), 1)) # 2. Text statistics (always useful, interpretable) stats_list = clean_texts.apply(self._get_text_statistics).tolist() stats_features = np.array([[s['char_count'], s['word_count'], s['avg_word_len'], s['sentence_count'], s['uppercase_ratio'], s['digit_ratio']] for s in stats_list]) logger.info(f" ✅ Text stats: 6 features") # 3. Sentiment (simple but effective) sentiment_list = clean_texts.apply(self._get_sentiment_features).tolist() sentiment_features = np.array([[s['positive_score'], s['negative_score'], s['sentiment_ratio'], s['polarity']] for s in sentiment_list]) logger.info(f" ✅ Sentiment: 4 features") # Combine all features all_features = np.hstack([tfidf_features, stats_features, sentiment_features]) logger.info(f" ✅ Total NLP features: {all_features.shape[1]}") return all_features def _get_nlp_models(self) -> Dict[str, Tuple[Any, Dict]]: """Get models optimized for NLP/text classification""" models = { # Naive Bayes - excellent for text 'MultinomialNB': ( MultinomialNB(), {'alpha': [0.01, 0.1, 0.5, 1.0]} ), 'ComplementNB': ( ComplementNB(), {'alpha': [0.01, 0.1, 0.5, 1.0]} ), # Linear models - fast for high-dimensional text 'LogisticRegression': ( LogisticRegression(max_iter=1000, random_state=42), {'C': [0.1, 1, 10], 'penalty': ['l1', 'l2'], 'solver': ['saga']} ), 'SGDClassifier': ( SGDClassifier(random_state=42, max_iter=1000, early_stopping=True), {'alpha': [0.0001, 0.001, 0.01], 'penalty': ['l1', 'l2', 'elasticnet']} ), 'LinearSVC': ( LinearSVC(random_state=42, max_iter=2000), {'C': [0.1, 1, 10]} ), 'PassiveAggressive': ( PassiveAggressiveClassifier(random_state=42, max_iter=1000), {'C': [0.01, 0.1, 1.0]} ), # Tree-based (for shorter texts) 'RandomForest': ( RandomForestClassifier(n_jobs=-1, random_state=42), {'n_estimators': [100, 200], 'max_depth': [10, 20]} ), # Neural Network 'MLPClassifier': ( MLPClassifier(random_state=42, max_iter=500, early_stopping=True), {'hidden_layer_sizes': [(100,), (100, 50)], 'alpha': [0.0001, 0.001]} ), } if HAS_XGB: models['XGBoost'] = ( xgb.XGBClassifier(n_jobs=-1, random_state=42, verbosity=0), {'n_estimators': [100], 'max_depth': [5, 10]} ) if HAS_LGB: models['LightGBM'] = ( lgb.LGBMClassifier(n_jobs=-1, random_state=42, verbose=-1), {'n_estimators': [100], 'max_depth': [5, 10]} ) return models def _detect_target_leakage(self, X: pd.DataFrame, y: pd.Series, threshold: float = 0.95) -> List[str]: """Detect features that perfectly predict target (data leakage)""" leaky_features = [] numeric_cols = X.select_dtypes(include=[np.number]).columns try: for col in numeric_cols: corr = abs(X[col].corr(y)) if corr > threshold: leaky_features.append(col) logger.warning(f" 🚨 LEAKAGE detected: '{col}' has {corr:.1%} correlation with target!") # SAFEGUARD: If ALL numeric features are considered leakage, don't drop them! # This happens in small datasets where features ARE the predictors if len(leaky_features) > 0 and len(leaky_features) == len(numeric_cols): logger.warning(f" ⚠️ WARNING: All numeric features are highly correlated (>95%). ") logger.warning(f" Keeping them to avoid empty dataset (might be small dataset or strong predictors).") return [] except: pass return leaky_features def _preprocess_training(self, df: pd.DataFrame, target_col: str) -> Tuple[np.ndarray, np.ndarray]: """Preprocess data for training - ENHANCED for messy real-world data and NLP""" logger.info("🧹 ADVANCED DATA CLEANING...") X = df.drop(columns=[target_col]).copy() y = df[target_col].copy() self.target_column = target_col # Detect task type task_info = self._detect_task_type(y) self.task_type = task_info[0] self.n_classes = task_info[1] self.task_type_simple = 'classification' if 'classification' in self.task_type else 'regression' logger.info(f" Task: {self.task_type} ({'n_classes=' + str(self.n_classes) if self.n_classes else 'continuous'})") # DETECT NLP DATASET self.is_nlp_task, self.primary_text_col = self._is_nlp_dataset(df, target_col) if self.is_nlp_task: logger.info(f" 📝 DETECTED: NLP/Text Classification Dataset") logger.info(f" 📝 Primary text column: {self.primary_text_col}") # STEP 1: Detect data leakage (features that perfectly predict target) y_numeric = pd.to_numeric(y, errors='coerce') if not y_numeric.isna().all(): leaky_cols = self._detect_target_leakage(X, y_numeric, threshold=0.95) if leaky_cols: X = X.drop(columns=leaky_cols) logger.info(f" 🚨 Removed {len(leaky_cols)} leaky features") # STEP 2: Remove highly correlated features (>95% correlation) correlated_cols = self._remove_highly_correlated(X, threshold=0.95) if correlated_cols: X = X.drop(columns=correlated_cols) # STEP 3: Select columns (drops IDs, dates, constants, etc.) self.numeric_cols, self.categorical_cols, self.text_cols, self.dropped_cols = \ self._select_columns(df.drop(columns=leaky_cols + correlated_cols if 'leaky_cols' in dir() else [], errors='ignore'), target_col) # Remove any dropped leaky/correlated columns from our selected columns self.numeric_cols = [c for c in self.numeric_cols if c in X.columns] self.categorical_cols = [c for c in self.categorical_cols if c in X.columns] self.text_cols = [c for c in self.text_cols if c in X.columns] self.feature_columns = self.numeric_cols + self.categorical_cols + self.text_cols if len(self.feature_columns) == 0: raise ValueError("No valid features found after column selection and cleaning") # Process features processed_parts = [] self.feature_metadata = [] # 1. NUMERIC FEATURES if self.numeric_cols: numeric_data = [] for col in self.numeric_cols: cleaned, fill_val = self._clean_numeric(X[col]) # Cap outliers using IQR method for robust predictions cleaned = self._cap_outliers(cleaned) self.numeric_fill_values[col] = fill_val numeric_data.append(cleaned) # Calculate percentiles for better display try: p25 = float(np.nanpercentile(cleaned, 25)) p75 = float(np.nanpercentile(cleaned, 75)) except: p25 = p75 = fill_val self.feature_metadata.append({ 'name': col, 'type': 'numeric', 'min': float(np.nanmin(cleaned)) if not np.isnan(np.nanmin(cleaned)) else 0, 'max': float(np.nanmax(cleaned)) if not np.isnan(np.nanmax(cleaned)) else 100, 'mean': float(np.nanmean(cleaned)) if not np.isnan(np.nanmean(cleaned)) else 50, 'default': float(fill_val), 'p25': p25, 'p75': p75, 'placeholder': f"{fill_val:.2f} (typical: {p25:.1f} - {p75:.1f})" }) numeric_array = np.column_stack(numeric_data) # Scale with RobustScaler (handles outliers better) self.scaler = RobustScaler() numeric_scaled = self.scaler.fit_transform(numeric_array) processed_parts.append(numeric_scaled) # 2. CATEGORICAL FEATURES for col in self.categorical_cols: series = X[col].fillna('_MISSING_').astype(str).str.strip() encoder = LabelEncoder() encoded = encoder.fit_transform(series).reshape(-1, 1) self.label_encoders[col] = encoder processed_parts.append(encoded.astype(float)) # Get most common value as default mode = series.mode()[0] if len(series.mode()) > 0 else encoder.classes_[0] self.feature_metadata.append({ 'name': col, 'type': 'categorical', 'options': encoder.classes_.tolist()[:50], 'default': mode, 'n_categories': len(encoder.classes_) }) # 3. TEXT FEATURES (Comprehensive NLP Processing) for col in self.text_cols.copy(): series = X[col].fillna('').astype(str) try: # Use comprehensive NLP extraction for NLP datasets or primary text column if getattr(self, 'is_nlp_task', False) or col == getattr(self, 'primary_text_col', None): # FULL NLP PIPELINE - sentiment, stats, TF-IDF with 200 features nlp_features = self._extract_nlp_features(series, col) # Handle negative values for Naive Bayes (important for sentiment scores -1 to 1) # using MinMax scaling to [0, 1] instead of abs() which destroys polarity from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler(feature_range=(0, 1)) nlp_features = scaler.fit_transform(nlp_features) self.nlp_scaler = scaler processed_parts.append(nlp_features) self.feature_metadata.append({ 'name': col, 'type': 'nlp_text', 'n_features': nlp_features.shape[1], 'has_sentiment': True }) else: # Standard text processing for non-primary text columns clean_texts = series.apply(self._clean_text_nlp) vectorizer = TfidfVectorizer( max_features=100, stop_words='english', ngram_range=(1, 3), min_df=2, max_df=0.9, lowercase=True, strip_accents='unicode' ) vectors = vectorizer.fit_transform(clean_texts).toarray() if vectors.shape[1] > 0: self.text_vectorizers[col] = vectorizer processed_parts.append(vectors) self.feature_metadata.append({ 'name': col, 'type': 'text', 'vocab_size': len(vectorizer.vocabulary_) }) logger.info(f" ✅ {col}: {len(vectorizer.vocabulary_)} text features") else: self.text_cols.remove(col) except Exception as e: logger.warning(f" ⚠️ Text processing failed for {col}: {str(e)[:50]}") # FALLBACK: Encode as categorical if text processing fails try: series = X[col].fillna('_MISSING_').astype(str).str.strip() encoder = LabelEncoder() encoded = encoder.fit_transform(series).reshape(-1, 1) self.label_encoders[col] = encoder processed_parts.append(encoded.astype(float)) logger.info(f" ✅ {col}: Fallback to categorical encoding ({len(encoder.classes_)} categories)") self.feature_metadata.append({ 'name': col, 'type': 'categorical', 'options': encoder.classes_.tolist()[:50], 'default': encoder.classes_[0], 'n_categories': len(encoder.classes_) }) except Exception as fallback_err: logger.warning(f" ❌ {col}: Could not encode, skipping: {fallback_err}") if col in self.text_cols: self.text_cols.remove(col) # Combine if processed_parts: X_processed = np.hstack(processed_parts) else: raise ValueError("No valid features after preprocessing") # === ADVANCED FEATURE ENGINEERING (Tabular Data) === try: # Only apply for tabular data without massive text vectors to avoid explosion if not self.is_nlp_task and len(self.text_cols) == 0 and not self.data_profile.get('is_large_data'): current_features = self.numeric_cols + self.categorical_cols # Ensure feature names match column count (LabelEnc=1, Numeric=1) # This check ensures we don't mismatch if something expanded unexpectedly if hasattr(X_processed, 'shape') and X_processed.shape[1] == len(current_features): logger.info(" 🔧 Applying Advanced Feature Engineering (Poly + Interactions)...") # 1. Polynomial Features if len(self.numeric_cols) > 0: X_processed, poly_names = self.feature_engineer.create_polynomial_features( X_processed, feature_names=current_features, degree=2, top_n=5 ) current_features = poly_names # 2. Interactions - SKIPPING explicit interactions as Tree models handle them # and we need a persistable transformer for prediction time. # GBMs (XGBoost/CatBoost) capture interactions automatically. except Exception as e: logger.warning(f" ⚠️ Advanced feature engineering skipped: {e}") # Clean final array - ensure numeric dtype before nan_to_num try: # Convert to float, handling any object types if X_processed.dtype == object: # 🛡️ ROBUST: Force column-by-column conversion for object arrays logger.warning(" ⚠️ Object dtype detected, forcing column-wise numeric conversion...") X_clean = np.zeros(X_processed.shape, dtype=float) for i in range(X_processed.shape[1]): try: X_clean[:, i] = pd.to_numeric(X_processed[:, i], errors='coerce').fillna(0) except Exception: try: # Last resort: LabelEncode strings like 'Absence', 'Present' le = LabelEncoder() X_clean[:, i] = le.fit_transform(X_processed[:, i].astype(str)) logger.info(f" ✅ Column {i}: Label encoded") except: X_clean[:, i] = 0 X_processed = X_clean X_processed = np.nan_to_num(X_processed.astype(float), nan=0.0, posinf=0.0, neginf=0.0) except (ValueError, TypeError) as conv_err: logger.warning(f" ⚠️ Numeric conversion issue: {conv_err}, attempting column-wise conversion") # Fallback: column-wise conversion with LabelEncoder X_clean = np.zeros_like(X_processed, dtype=float) for i in range(X_processed.shape[1]): try: col = pd.to_numeric(X_processed[:, i], errors='coerce') X_clean[:, i] = np.nan_to_num(col, nan=0.0, posinf=0.0, neginf=0.0) except Exception: try: le = LabelEncoder() X_clean[:, i] = le.fit_transform(X_processed[:, i].astype(str)) except: X_clean[:, i] = 0.0 X_processed = X_clean # Process target if self.task_type_simple == 'classification': self.target_encoder = LabelEncoder() y_processed = self.target_encoder.fit_transform(y.fillna('_MISSING_').astype(str).str.strip()) else: y_processed = pd.to_numeric(y, errors='coerce').fillna(0).values.astype(float) y_processed = np.nan_to_num(y_processed, nan=0.0) logger.info(f" Final shape: {X_processed.shape}") return X_processed, y_processed def _preprocess_single(self, data: Dict[str, Any]) -> np.ndarray: """Preprocess single input for prediction - LEGACY PIPELINE""" logger.info(f"🔧 Legacy preprocessing: {len(data)} input features") parts = [] # Numeric if self.numeric_cols: logger.info(f" Processing {len(self.numeric_cols)} numeric columns") numeric_vals = [] for col in self.numeric_cols: val = data.get(col, self.numeric_fill_values.get(col, 0)) try: numeric_vals.append(float(val) if val is not None else self.numeric_fill_values.get(col, 0)) except: numeric_vals.append(self.numeric_fill_values.get(col, 0)) numeric_array = np.array([numeric_vals]) if self.scaler: parts.append(self.scaler.transform(numeric_array)) else: parts.append(numeric_array) logger.info(f" Numeric features shape: {parts[-1].shape if parts else 'none'}") # Categorical if self.categorical_cols: logger.info(f" Processing {len(self.categorical_cols)} categorical columns") for col in self.categorical_cols: encoder = self.label_encoders.get(col) if encoder: val = str(data.get(col, '_MISSING_')).strip() try: if hasattr(encoder, 'classes_') and val in encoder.classes_: encoded = encoder.transform([val])[0] else: encoded = 0 # Unknown category defaults to 0 except Exception as e: logger.warning(f" ⚠️ Encoding failed for {col}={val}: {e}") encoded = 0 parts.append(np.array([[float(encoded)]])) # Text for col in self.text_cols: text = str(data.get(col, '')) # Check if this column was processed with full NLP pipeline if getattr(self, 'is_nlp_task', False) or col == getattr(self, 'primary_text_col', None): # Use saved TF-IDF vectorizer for consistent feature extraction vectorizer = self.text_vectorizers.get(col) if vectorizer: try: # 1. TF-IDF features from saved vectorizer tfidf_sparse = vectorizer.transform([text]) # 2. Apply SVD if it was used during training svd_transformers = getattr(self, 'text_svd_transformers', {}) if col in svd_transformers: svd = svd_transformers[col] tfidf_feats = svd.transform(tfidf_sparse) else: tfidf_feats = tfidf_sparse.toarray() parts.append(tfidf_feats) except Exception as e: logger.warning(f" ⚠️ TF-IDF/SVD transform failed for {col}: {e}") # Fallback to expected SVD size or vocab size svd_transformers = getattr(self, 'text_svd_transformers', {}) if col in svd_transformers: expected_size = svd_transformers[col].n_components else: expected_size = len(vectorizer.vocabulary_) if hasattr(vectorizer, 'vocabulary_') else 50 parts.append(np.zeros((1, expected_size))) # 2. Text stats (same as training) clean_text = self._clean_text_nlp(text) stats = self._get_text_statistics(clean_text) stats_features = np.array([[stats['char_count'], stats['word_count'], stats['avg_word_len'], stats['sentence_count'], stats['uppercase_ratio'], stats['digit_ratio']]]) parts.append(stats_features) # 3. Sentiment features (same as training) sentiment = self._get_sentiment_features(clean_text) sentiment_features = np.array([[sentiment['positive_score'], sentiment['negative_score'], sentiment['sentiment_ratio'], sentiment['polarity']]]) parts.append(sentiment_features) # Fallback to standard vectorizer if it exists (non-NLP text) elif col in self.text_vectorizers: vectorizer = self.text_vectorizers[col] tfidf_sparse = vectorizer.transform([text]) # Check for SVD svd_transformers = getattr(self, 'text_svd_transformers', {}) if col in svd_transformers: parts.append(svd_transformers[col].transform(tfidf_sparse)) else: parts.append(tfidf_sparse.toarray()) X_processed = np.hstack(parts) if parts else np.array([[0]]) logger.info(f" Combined features shape before poly: {X_processed.shape}") # === Apply Advanced Feature Engineering (Poly) === try: if self.feature_engineer.poly_transformer is not None: # PolynomialFeatures expects 'n_features_in_' columns. # Since we fit it on the first N columns of the processed array during training # We must slice the same way here. n_input = self.feature_engineer.poly_transformer.n_features_in_ if X_processed.shape[1] >= n_input: X_subset = X_processed[:, :n_input] X_poly = self.feature_engineer.poly_transformer.transform(X_subset) # Combine: If train expanded, we expand. # Note: create_polynomial_features combined logic: # if n_features < X.shape[1]: combined = hstack([poly, rest]) # else: combined = poly if n_input < X_processed.shape[1]: X_processed = np.hstack([X_poly, X_processed[:, n_input:]]) else: X_processed = X_poly except Exception as e: logger.warning(f"⚠️ Prediction feature engineering error: {e}") # === FEATURE DIMENSION VALIDATION === if hasattr(self, 'model') and hasattr(self.model, 'n_features_in_'): expected = self.model.n_features_in_ actual = X_processed.shape[1] if actual != expected: logger.warning(f"⚠️ FEATURE MISMATCH: Model expects {expected}, got {actual}") # Try to fix by padding or truncating if actual < expected: padding = np.zeros((1, expected - actual)) X_processed = np.hstack([X_processed, padding]) logger.info(f" Padded to {X_processed.shape[1]} features") else: X_processed = X_processed[:, :expected] logger.info(f" Truncated to {X_processed.shape[1]} features") else: logger.info(f" ✅ Feature count matches: {actual}") logger.info(f" Final preprocessed shape: {X_processed.shape}") return X_processed # ========================================================================= # MODELS # ========================================================================= def _get_models(self) -> Dict[str, Tuple[Any, Dict]]: """Get ALL available ML models with hyperparameter grids - COMPREHENSIVE""" if self.task_type_simple == 'classification': models = { # === LINEAR MODELS === 'LogisticRegression': ( LogisticRegression(max_iter=1000, n_jobs=-1, random_state=42), {'C': [0.01, 0.1, 1, 10], 'penalty': ['l1', 'l2'], 'solver': ['saga']} ), 'SGDClassifier': ( SGDClassifier(random_state=42, max_iter=1000), {'alpha': [0.0001, 0.001, 0.01], 'penalty': ['l1', 'l2', 'elasticnet']} ), # === TREE-BASED MODELS === 'DecisionTree': ( DecisionTreeClassifier(random_state=42), {'max_depth': [5, 10, 20, None], 'min_samples_split': [2, 5, 10]} ), 'RandomForest': ( RandomForestClassifier(n_jobs=-1, random_state=42), {'n_estimators': [100, 200], 'max_depth': [10, 20, None], 'min_samples_split': [2, 5]} ), 'ExtraTrees': ( ExtraTreesClassifier(n_jobs=-1, random_state=42), {'n_estimators': [100, 200], 'max_depth': [10, 20, None]} ), 'HistGradientBoosting': ( HistGradientBoostingClassifier(random_state=42), {'max_iter': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ), 'AdaBoost': ( AdaBoostClassifier(random_state=42), {'n_estimators': [50, 100, 200], 'learning_rate': [0.01, 0.1, 1.0]} ), # === SVM MODELS === 'SVM': ( SVC(random_state=42, probability=True), {'C': [0.1, 1, 10], 'kernel': ['rbf', 'linear']} ), # === NAIVE BAYES (great for NLP/text) === 'GaussianNB': ( GaussianNB(), {'var_smoothing': [1e-9, 1e-8, 1e-7]} ), 'BernoulliNB': ( BernoulliNB(), {'alpha': [0.1, 0.5, 1.0], 'binarize': [0.0, 0.5]} ), # === DISCRIMINANT ANALYSIS === 'LDA': ( LinearDiscriminantAnalysis(), {'solver': ['svd', 'lsqr']} ), 'QDA': ( QuadraticDiscriminantAnalysis(), {'reg_param': [0.0, 0.1, 0.5]} ), # === NEURAL NETWORK === 'MLPClassifier': ( MLPClassifier(random_state=42, max_iter=500, early_stopping=True), {'hidden_layer_sizes': [(50,), (100,), (100, 50)], 'alpha': [0.0001, 0.001]} ), # === KNN === 'KNN': ( KNeighborsClassifier(n_jobs=-1), {'n_neighbors': [3, 5, 7, 11], 'weights': ['uniform', 'distance']} ), } # Gradient Boosting libraries if HAS_XGB: models['XGBoost'] = ( xgb.XGBClassifier(n_jobs=-1, random_state=42, verbosity=0, eval_metric='logloss'), {'n_estimators': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ) if HAS_LGB: models['LightGBM'] = ( lgb.LGBMClassifier(n_jobs=-1, random_state=42, verbose=-1), {'n_estimators': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ) if HAS_CATBOOST: models['CatBoost'] = ( CatBoostClassifier(random_state=42, verbose=0, thread_count=-1), {'iterations': [100, 200], 'depth': [6, 8], 'learning_rate': [0.05, 0.1]} ) else: # REGRESSION models = { # === LINEAR MODELS === 'Ridge': ( Ridge(random_state=42), {'alpha': [0.01, 0.1, 1, 10, 100]} ), 'ElasticNet': ( ElasticNet(random_state=42, max_iter=2000), {'alpha': [0.01, 0.1, 1], 'l1_ratio': [0.2, 0.5, 0.8]} ), 'Lasso': ( Lasso(random_state=42, max_iter=2000), {'alpha': [0.01, 0.1, 1, 10]} ), 'SGDRegressor': ( SGDRegressor(random_state=42, max_iter=1000), {'alpha': [0.0001, 0.001, 0.01], 'penalty': ['l1', 'l2', 'elasticnet']} ), 'BayesianRidge': ( BayesianRidge(), {'alpha_1': [1e-6, 1e-5], 'lambda_1': [1e-6, 1e-5]} ), 'HuberRegressor': ( HuberRegressor(max_iter=1000), {'epsilon': [1.1, 1.35, 1.5, 1.75], 'alpha': [0.0001, 0.001]} ), 'PoissonRegressor': ( PoissonRegressor(max_iter=1000), {'alpha': [0.1, 1.0, 10.0]} ), # === TREE-BASED MODELS === 'DecisionTree': ( DecisionTreeRegressor(random_state=42), {'max_depth': [5, 10, 20, None], 'min_samples_split': [2, 5, 10]} ), 'RandomForest': ( RandomForestRegressor(n_jobs=-1, random_state=42), {'n_estimators': [100, 200], 'max_depth': [10, 20, None], 'min_samples_split': [2, 5]} ), 'ExtraTrees': ( ExtraTreesRegressor(n_jobs=-1, random_state=42), {'n_estimators': [100, 200], 'max_depth': [10, 20, None]} ), 'HistGradientBoosting': ( HistGradientBoostingRegressor(random_state=42), {'max_iter': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ), 'AdaBoost': ( AdaBoostRegressor(random_state=42), {'n_estimators': [50, 100, 200], 'learning_rate': [0.01, 0.1, 1.0]} ), # === SVM === 'SVR': ( SVR(), {'C': [0.1, 1, 10], 'kernel': ['rbf', 'linear']} ), # === NEURAL NETWORK === 'MLPRegressor': ( MLPRegressor(random_state=42, max_iter=500, early_stopping=True), {'hidden_layer_sizes': [(50,), (100,), (100, 50)], 'alpha': [0.0001, 0.001]} ), # === KNN === 'KNN': ( KNeighborsRegressor(n_jobs=-1), {'n_neighbors': [3, 5, 7, 11], 'weights': ['uniform', 'distance']} ), } # Gradient Boosting libraries if HAS_XGB: models['XGBoost'] = ( xgb.XGBRegressor(n_jobs=-1, random_state=42, verbosity=0), {'n_estimators': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ) if HAS_LGB: models['LightGBM'] = ( lgb.LGBMRegressor(n_jobs=-1, random_state=42, verbose=-1), {'n_estimators': [100, 200], 'max_depth': [5, 10], 'learning_rate': [0.05, 0.1]} ) if HAS_CATBOOST: models['CatBoost'] = ( CatBoostRegressor(random_state=42, verbose=0, thread_count=-1), {'iterations': [100, 200], 'depth': [6, 8], 'learning_rate': [0.05, 0.1]} ) return models return models # ========================================================================= # ADVANCED METHODS FOR TOP 1% ACCURACY # ========================================================================= def _handle_class_imbalance(self, X_train: np.ndarray, y_train: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """Handle class imbalance using SMOTE if available""" if not HAS_IMBLEARN or self.task_type_simple != 'classification': return X_train, y_train try: class_counts = np.bincount(y_train.astype(int)) if len(class_counts) < 2: return X_train, y_train min_class_count = min(class_counts[class_counts > 0]) # Avoid zero counts imbalance_ratio = max(class_counts) / max(min_class_count, 1) if imbalance_ratio > 3 and min_class_count >= 6: print(f" ⚠️ Class imbalance detected (ratio={imbalance_ratio:.1f}:1), applying SMOTE...") # k_neighbors must be less than the smallest class size k_neighbors = min(5, min_class_count - 1) if k_neighbors < 1: print(f" ⚠️ Too few samples in minority class for SMOTE") return X_train, y_train # For multiclass, use 'auto' strategy (float only works for binary) n_classes = len(class_counts[class_counts > 0]) sampling_strategy = 'auto' if n_classes > 2 else 'auto' smote = SMOTE( random_state=42, k_neighbors=k_neighbors, sampling_strategy=sampling_strategy ) X_resampled, y_resampled = smote.fit_resample(X_train, y_train) print(f" ✅ Resampled: {len(X_train)} → {len(X_resampled)} samples") return X_resampled, y_resampled except Exception as e: print(f" ⚠️ SMOTE failed: {e}") return X_train, y_train def _optimize_with_optuna( self, model_name: str, X_train: np.ndarray, y_train: np.ndarray, n_trials: int = 30 ) -> Tuple[Any, Dict]: """Bayesian hyperparameter optimization with Optuna""" if not HAS_OPTUNA: return None, {} scoring = 'f1_weighted' if self.task_type_simple == 'classification' else 'r2' cv = StratifiedKFold(n_splits=3, shuffle=True, random_state=42) if self.task_type_simple == 'classification' else KFold(n_splits=3, shuffle=True, random_state=42) def objective(trial): if model_name == 'XGBoost' and HAS_XGB: params = { 'n_estimators': trial.suggest_int('n_estimators', 100, 500), 'max_depth': trial.suggest_int('max_depth', 3, 15), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True), 'subsample': trial.suggest_float('subsample', 0.6, 1.0), 'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0), 'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10.0, log=True), 'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10.0, log=True), } if self.task_type_simple == 'classification': model = xgb.XGBClassifier(**params, n_jobs=-1, random_state=42, verbosity=0, eval_metric='logloss') else: model = xgb.XGBRegressor(**params, n_jobs=-1, random_state=42, verbosity=0) elif model_name == 'LightGBM' and HAS_LGB: params = { 'n_estimators': trial.suggest_int('n_estimators', 100, 500), 'max_depth': trial.suggest_int('max_depth', 3, 15), 'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True), 'num_leaves': trial.suggest_int('num_leaves', 20, 150), 'min_child_samples': trial.suggest_int('min_child_samples', 5, 100), } if self.task_type_simple == 'classification': model = lgb.LGBMClassifier(**params, n_jobs=-1, random_state=42, verbose=-1) else: model = lgb.LGBMRegressor(**params, n_jobs=-1, random_state=42, verbose=-1) elif model_name == 'RandomForest': params = { 'n_estimators': trial.suggest_int('n_estimators', 100, 400), 'max_depth': trial.suggest_int('max_depth', 5, 30), 'min_samples_split': trial.suggest_int('min_samples_split', 2, 20), 'min_samples_leaf': trial.suggest_int('min_samples_leaf', 1, 10), } if self.task_type_simple == 'classification': model = RandomForestClassifier(**params, n_jobs=-1, random_state=42) else: model = RandomForestRegressor(**params, n_jobs=-1, random_state=42) else: return 0.0 try: scores = cross_val_score(model, X_train, y_train, cv=cv, scoring=scoring, n_jobs=1) return scores.mean() except: return 0.0 try: study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=n_trials, show_progress_bar=False, n_jobs=1) logger.info(f" 🎯 Optuna best score: {study.best_value:.4f}") return study.best_params, study.best_value except Exception as e: logger.warning(f" ⚠️ Optuna failed: {e}") return None, {} def _build_stacking_ensemble( self, trained_models: Dict[str, Any], X_train: np.ndarray, y_train: np.ndarray ) -> Any: """Build stacking ensemble from trained models""" if len(trained_models) < 2: return None try: # Select top 3 models for stacking estimators = [(name, model) for name, model in list(trained_models.items())[:3]] logger.info(f" 🏗️ Building stacking ensemble with {len(estimators)} models...") if self.task_type_simple == 'classification': meta_learner = LogisticRegression(max_iter=1000, random_state=42) stacker = StackingClassifier( estimators=estimators, final_estimator=meta_learner, cv=3, passthrough=False, n_jobs=1 ) else: meta_learner = Ridge(random_state=42) stacker = StackingRegressor( estimators=estimators, final_estimator=meta_learner, cv=3, passthrough=False, n_jobs=1 ) stacker.fit(X_train, y_train) logger.info(f" ✅ Stacking ensemble trained successfully") return stacker except Exception as e: logger.warning(f" ⚠️ Stacking failed: {e}") return None def _get_cv_strategy(self, y: np.ndarray, n_splits: int = 5): """Intelligent CV strategy selection""" from sklearn.model_selection import StratifiedKFold, KFold n_samples = len(y) # SAFEGUARD: Ensure splits don't exceed samples if n_splits > n_samples: n_splits = max(2, min(3, n_samples)) if self.task_type_simple == 'classification': # Stratified maintains class balance try: # If any class has fewer members than n_splits, reduce splits min_class_members = pd.Series(y).value_counts().min() if min_class_members < n_splits: n_splits = max(2, min_class_members) except: pass return StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) else: # Regression: ALWAYS use KFold, never Stratified return KFold(n_splits=n_splits, shuffle=True, random_state=42) # ========================================================================= # PRODUCTION TRAINING (AUTOML) # ========================================================================= async def train_with_test_set( self, train_df: pd.DataFrame, test_df: pd.DataFrame, target_col: Optional[str] = None, user_id: str = "default" ) -> 'TrainResult': """ Wrapper to handle separate Test set from API. Concatenates data and runs production pipeline to ensure consistent processing. """ logger.info(f"🔄 train_with_test_set called: Train={len(train_df)}, Test={len(test_df)}") # Calculate Metadata on Train set specifically (to capture raw distributions) # We do this before concat to ensure we capture training distribution self._calculate_feature_metadata(train_df) # Concatenate for full AutoML logic (which handles splitting internally) # Note: We rely on production_train's internal robust validation full_df = pd.concat([train_df, test_df], ignore_index=True) return self.production_train(full_df, target_col, user_id) def production_train( self, df: pd.DataFrame, target_col: Optional[str] = None, user_id: str = "default", mode: str = "fast", # 'fast' or 'ultra' algorithm: Optional[str] = None # Specific algorithm or None for auto-select ) -> 'TrainResult': """ 🚀 PRODUCTION AUTOML PIPELINE Uses the production_ml_core module for: - Smart data cleaning - Advanced feature engineering - 15+ production algorithms (XGBoost, LightGBM, CatBoost, etc.) - Ensemble methods for best accuracy If algorithm is specified, trains only that algorithm. Otherwise, trains multiple and picks the best. Expected accuracy: 80%+ """ from sklearn.model_selection import train_test_split # Reset cancellation flag for this user CANCELLATION_FLAGS[user_id] = False cleaned_file_path = None # Track cleaned file path # Callback wrapper to allow cancellation during training def check_stop(): check_cancellation(user_id) self.errors = [] start = datetime.now() logger.info("=" * 60) logger.info("🚀 PRODUCTION AUTOML PIPELINE") logger.info("=" * 60) # 🆕 Use Smart Data Analyzer for improved target detection and insights try: from ml.smart_data_analyzer import SmartDataAnalyzer analyzer = SmartDataAnalyzer() analysis = analyzer.analyze(df, target_col) # Use detected target if none provided if not target_col: target_col = analysis.target_column logger.info(f"📊 Data Type: {analysis.data_type.value.upper()}") logger.info(f"🎪 Task Type: {analysis.task_type.value}") logger.info(f"🔧 Mode: {mode.upper()}") except Exception as e: logger.warning(f"Smart analyzer unavailable: {e}") if not target_col: target_col = self._detect_target(df) logger.info(f"📊 Data: {df.shape[0]} rows, {df.shape[1]} columns") logger.info(f"🎯 Target: {target_col}") # CRITICAL: Store target column for metadata persistence self.target_column = target_col # 1. Clean data cleaner = ProductionDataCleaner() df_clean = cleaner.clean(df, target_col) # CRITICAL: Drop rows where target is NaN (cleaner skips target column) target_nan_count = df_clean[target_col].isna().sum() if target_nan_count > 0: df_clean = df_clean.dropna(subset=[target_col]) logger.warning(f" ⚠️ Dropped {target_nan_count} rows with missing target values") # SAVE CLEANED DATA (User Request) # SAVE CLEANED DATA (User Request) try: from utils.paths import get_user_paths user_paths = get_user_paths(user_id) # Save to standard 'files' storage so /api/v1/files endpoint can serve it upload_dir = user_paths['files'] cleaned_filename = f"cleaned_{int(datetime.now().timestamp())}.csv" cleaned_full_path = upload_dir / cleaned_filename df_clean.to_csv(cleaned_full_path, index=False) logger.info(f"💾 Saved cleaned data to: {cleaned_full_path}") cleaned_file_path = cleaned_filename except Exception as e: logger.error(f"Failed to save cleaned data: {e}") cleaned_file_path = None # 2. Detect task type (IMPROVED - handles Rating columns with decimals correctly) y_temp = df_clean[target_col] n_unique = y_temp.nunique() # CRITICAL FIX: First try to convert to numeric, then check types y_numeric = pd.to_numeric(y_temp, errors='coerce') valid_numeric_ratio = y_numeric.notna().sum() / len(y_temp) is_numeric = valid_numeric_ratio > 0.5 # >50% are valid numbers # Check if values are continuous decimals (like ratings 4.1, 4.2) is_decimal = False if is_numeric and y_numeric.notna().any(): try: # Use the converted numeric values non_null = y_numeric.dropna() # Check if any value has decimal part is_decimal = (non_null % 1 != 0).any() if is_decimal: logger.info(f" 🔍 Detected decimal values in target (e.g., {non_null.iloc[0]})") except Exception as e: logger.warning(f" ⚠️ Decimal check failed: {e}") # IMPROVED LOGIC: # 1. If target has decimals AND >10 unique values -> ALWAYS Regression (ratings, prices) # 2. If target is NOT numeric (strings) -> Classification # 3. If target has few unique values (<20) -> Classification # 🆕 OVERRIDE: Use Smart Data Analyzer result if available if 'analysis' in locals() and analysis: smart_task = analysis.task_type.value if 'classification' in smart_task: self.task_type = 'classification' self.task_type_simple = 'classification' logger.info(f"🎯 Smart Analysis: Detected CLASSIFICATION ({smart_task})") elif 'regression' in smart_task: self.task_type = 'regression' self.task_type_simple = 'regression' logger.info(f"🎯 Smart Analysis: Detected REGRESSION ({smart_task})") # Fallback to heuristic if Smart Analysis didn't run or was inconclusive elif is_decimal and n_unique > 10: self.task_type = 'regression' self.task_type_simple = 'regression' logger.info(f"📋 Task: REGRESSION (decimal values detected)") elif not is_numeric: self.task_type = 'classification' self.task_type_simple = 'classification' logger.info(f"📋 Task: CLASSIFICATION (non-numeric target)") elif n_unique < 20: self.task_type = 'classification' self.task_type_simple = 'classification' logger.info(f"📋 Task: CLASSIFICATION (low cardinality: {n_unique})") else: # Default to regression for many unique numeric values self.task_type = 'regression' self.task_type_simple = 'regression' logger.info(f"📋 Task: REGRESSION (default, {n_unique} unique)") # 3. Feature engineering (pass mode for Fast vs Ultra NLP) engineer = ProductionFeatureEngineer(mode=mode) # EXTRACT METADATA FOR PREDICTIONS TAB (INPUT SCHEMA) -- # This must capture ORIGINAL columns BEFORE feature engineering! # Must match EXACTLY how NLP engine does it for consistency self.feature_metadata = [] # Columns to skip (ID columns, index columns, internal columns) skip_patterns = ['unnamed', 'index', '_id', 'id'] for col in df_clean.columns: if col == target_col: continue # Skip ID/index columns - they shouldn't be user inputs col_lower = col.lower().strip() if col_lower in skip_patterns or col_lower.startswith('unnamed'): continue if col_lower == 'id' and df_clean[col].nunique() == len(df_clean): # Skip if it's a unique ID column continue # Detect column type: numeric, text, datetime, or categorical # Use SAME logic as NLP engine for consistency # Check for datetime first if pd.api.types.is_datetime64_any_dtype(df_clean[col]): # Datetime column - treat as date input self.feature_metadata.append({ 'name': col, 'type': 'date', 'format': 'YYYY-MM-DD' }) elif pd.api.types.is_numeric_dtype(df_clean[col]): col_type = 'numeric' meta = { 'name': col, 'type': 'numeric' } try: meta['min'] = float(df_clean[col].min()) meta['max'] = float(df_clean[col].max()) meta['mean'] = float(df_clean[col].mean()) except: meta['min'] = 0 meta['max'] = 100 meta['mean'] = 50 self.feature_metadata.append(meta) elif df_clean[col].dtype == 'object' or str(df_clean[col].dtype) == 'string': # String column - detect text vs categorical vs date using NLP engine logic sample = df_clean[col].dropna().astype(str) if len(sample) > 0: avg_len = sample.str.len().mean() unique_ratio = df_clean[col].nunique() / len(df_clean) # First check if it looks like a date column is_date_like = False col_lower = col.lower() if any(kw in col_lower for kw in ['date', 'time', 'created', 'updated', 'timestamp']): # Column name suggests date, try parsing try: pd.to_datetime(sample.head(10), errors='raise') is_date_like = True except: pass elif unique_ratio > 0.5 and avg_len <= 25: # High unique ratio and short strings - might be dates date_patterns = ['/', '-', ':'] if any(any(pat in str(v) for pat in date_patterns) for v in sample.head(5)): try: pd.to_datetime(sample.head(10), errors='raise') is_date_like = True except: pass if is_date_like: self.feature_metadata.append({ 'name': col, 'type': 'date', 'format': 'YYYY-MM-DD' }) elif avg_len < 30 and unique_ratio < 0.5: # Categorical dropdown try: options = df_clean[col].dropna().unique().tolist()[:50] self.feature_metadata.append({ 'name': col, 'type': 'categorical', 'options': [str(x) for x in options] }) except: self.feature_metadata.append({ 'name': col, 'type': 'categorical', 'options': [] }) else: # Text input (long text or high uniqueness like titles) self.feature_metadata.append({ 'name': col, 'type': 'text', 'placeholder': f'Enter {col}...' }) else: self.feature_metadata.append({ 'name': col, 'type': 'text', 'placeholder': f'Enter {col}...' }) else: # Other types - check for datetime-like strings or treat as categorical # Try to detect if this looks like a date column is_date_like = False try: sample = df_clean[col].dropna().head(10).astype(str) date_patterns = ['/', '-', ':'] if any(any(pat in str(v) for pat in date_patterns) for v in sample): # Try parsing as date try: pd.to_datetime(sample, errors='raise') is_date_like = True except: pass except: pass if is_date_like: self.feature_metadata.append({ 'name': col, 'type': 'date', 'format': 'YYYY-MM-DD' }) else: # Treat as categorical try: options = df_clean[col].dropna().unique().tolist()[:50] self.feature_metadata.append({ 'name': col, 'type': 'categorical', 'options': [str(x) for x in options] }) except: self.feature_metadata.append({ 'name': col, 'type': 'text', 'placeholder': f'Enter {col}...' }) logger.info(f" Feature metadata: {len(self.feature_metadata)} features for Predict tab") # -------------------------------------------------------- # 🛡️ CRITICAL FIX: SPLIT DATA BEFORE FEATURE ENGINEERING # This prevents TARGET ENCODING LEAKAGE where test target values # contaminate the categorical encoding logger.info("\n📊 TRAIN-TEST SPLIT (BEFORE FEATURE ENGINEERING)") logger.info("=" * 50) logger.info(" 🛡️ Splitting BEFORE feature engineering to prevent target leakage") # 4. Encode target for classification FIRST (before split) if self.task_type_simple == 'classification': self.target_encoder = LabelEncoder() df_clean[target_col] = self.target_encoder.fit_transform(df_clean[target_col].astype(str)) self.n_classes = len(self.target_encoder.classes_) # 5. Split data (with fallback for rare classes) try: stratify = df_clean[target_col] if self.task_type_simple == 'classification' else None df_train, df_test = train_test_split( df_clean, test_size=0.2, random_state=42, stratify=stratify ) except ValueError as e: # Fallback: use regular split if stratified fails (rare classes) logger.warning(f" ⚠️ Stratified split failed, using regular split: {str(e)[:50]}") df_train, df_test = train_test_split( df_clean, test_size=0.2, random_state=42 ) logger.info(f" ✅ Train: {len(df_train)} rows | Test: {len(df_test)} rows") # 6. Feature engineering - FIT on TRAINING DATA ONLY logger.info("\n🔧 FEATURE ENGINEERING (TRAIN DATA ONLY)") logger.info("=" * 50) logger.info(" 🛡️ Target encoding uses ONLY training data - NO LEAKAGE") X_train, y_train, feature_names = engineer.fit_transform(df_train, target_col, self.task_type_simple) self.feature_columns = feature_names # 7. Transform test data using FITTED transformers (no target leakage) logger.info("\n🔮 TRANSFORMING TEST DATA") X_test = engineer.transform(df_test, target_col) y_test = df_test[target_col].values if self.task_type_simple == 'regression': y_test = y_test.astype(float) else: y_test = y_test.astype(int) # Already encoded logger.info(f" ✅ X_train: {X_train.shape} | X_test: {X_test.shape}") # 8. Train all models # Pass mode to trainer ('fast' = 8 models, 'ultra' = 20+ with ensembles) # 🆕 Pass sample count AND feature count for Production Intelligence optimization n_features = X_train.shape[1] if len(X_train.shape) > 1 else 1 trainer = ProductionModelTrainer( self.task_type_simple, mode=mode, n_samples=len(X_train), n_features=n_features # 🛡️ PRODUCTION INTELLIGENCE ) logger.info(f"🎮 Training Mode: {mode.upper()}") # 🆕 Log large dataset detection if len(X_train) > 50000: logger.info(f" 📊 LARGE DATASET DETECTED: {len(X_train):,} training samples") logger.info(f" 📊 Optimizing models for speed & memory efficiency") # Check cancellation before heavy training phase check_stop() results = trainer.train_all(X_train, y_train, X_test, y_test, check_cancellation=check_stop) # 9. Build ensemble ensemble = trainer.build_ensemble(X_train, y_train, X_test, y_test, top_n=3) # 8. Neural Architecture Search (Ultra Mode Only) # 🆕 Skip for very large datasets (memory intensive) if mode == 'ultra' and len(X_train) <= 100000: try: from ml.neural_architecture_engine import train_neural_models logger.info("🧠 Starting Neural Architecture Search (Ultra Mode)...") # Use subset for responsiveness if data is massive if len(X_train) > 20000: indices = np.random.choice(len(X_train), 20000, replace=False) X_neural, y_neural = X_train[indices], y_train[indices] else: X_neural, y_neural = X_train, y_train neural_results = train_neural_models( X_neural, y_neural, X_test[:5000], y_test[:5000], # Validation set task_type=self.task_type_simple, n_classes=len(np.unique(y_train)) if self.task_type_simple == 'classification' else 0, check_cancellation=check_stop, max_epochs=30 # Quick search ) if neural_results.success: logger.info(f" 🧠 Best Neural Model: {neural_results.best_model_name} (Score: {neural_results.best_score:.4f})") # Add to leaderboard for visibility for res in neural_results.all_results: # Construct metrics dict metrics = { 'score': res['score'], 'val_loss': res.get('val_loss', 0) } if self.task_type_simple == 'classification': metrics['f1'] = res['score'] else: metrics['r2'] = res['score'] trainer.results.append({ 'name': f"DNN_{res['name']}", 'model': None, # Don't store Keras model to avoid pickle issues 'score': res['score'], 'metrics': metrics, 'time': neural_results.total_time_seconds / len(neural_results.all_results) }) # Re-sort leaderboard trainer.results.sort(key=lambda x: x['score'], reverse=True) except Exception as e: logger.warning(f"Neural engine skipped: {e}") elif mode == 'ultra' and len(X_train) > 100000: logger.info(" ⚠️ Skipping Neural Architecture Search for large dataset (>100k rows)") logger.info(" 📊 Using gradient boosting models (LightGBM, XGBoost) for best performance") # CRITICAL: Validate that training produced at least one model if trainer.best_model is None or not trainer.results: error_msg = "No models were successfully trained. Check your data quality." if hasattr(trainer, 'failed_models') and trainer.failed_models: error_msg = f"All models failed: {'; '.join(str(f) for f in trainer.failed_models[:3])}" logger.error(f"❌ {error_msg}") raise ValueError(error_msg) # Store results self.model = trainer.best_model self.model_name = trainer.best_name self._y_test = y_test self._y_pred = trainer.best_model.predict(X_test) # IMPORTANT: Store production feature engineer for predictions self.production_engineer = engineer self.production_mode = True # Get probabilities if classification y_proba = None if self.task_type_simple == 'classification' and hasattr(self.model, 'predict_proba'): try: y_proba = self.model.predict_proba(X_test) except: pass self._y_proba = y_proba # Save model self._save(user_id) elapsed = (datetime.now() - start).total_seconds() logger.info("\n" + "=" * 60) logger.info(f"✅ COMPLETE in {elapsed:.1f}s") logger.info(f"🏆 Best Model: {trainer.best_name}") logger.info(f"📈 Score: {trainer.best_score:.4f}") logger.info("=" * 60) # Get best metrics - for Ensemble, calculate directly best_result = next((r for r in trainer.results if r['name'] == trainer.best_name), None) if best_result: best_metrics = best_result.get('metrics', {}) else: # Ensemble or model not in results - calculate metrics directly from sklearn.metrics import accuracy_score, f1_score, r2_score, mean_absolute_error, precision_score, recall_score, mean_squared_error if self.task_type_simple == 'classification': acc = accuracy_score(y_test, self._y_pred) f1 = f1_score(y_test, self._y_pred, average='weighted', zero_division=0) prec = precision_score(y_test, self._y_pred, average='weighted', zero_division=0) rec = recall_score(y_test, self._y_pred, average='weighted', zero_division=0) best_metrics = { 'accuracy': round(acc, 4), 'f1': round(f1, 4), 'precision': round(prec, 4), 'recall': round(rec, 4) } # Add ROC-AUC try: n_classes = len(np.unique(y_test)) if n_classes == 2 and self._y_proba is not None: best_metrics['roc_auc'] = round(float(roc_auc_score(y_test, self._y_proba[:, 1])), 4) elif n_classes > 2 and self._y_proba is not None: best_metrics['roc_auc'] = round(float(roc_auc_score( y_test, self._y_proba, multi_class='ovr', average='weighted' )), 4) except Exception: pass else: r2 = r2_score(y_test, self._y_pred) mae = mean_absolute_error(y_test, self._y_pred) mse = mean_squared_error(y_test, self._y_pred) best_metrics = { 'r2': round(r2, 4), 'mse': round(mse, 4), 'rmse': round(float(np.sqrt(mse)), 4), 'mae': round(mae, 4) } # IMPORTANT: Store metrics on self so they're saved to persistence self.metrics = best_metrics # Generate charts charts = {} try: from ml.chart_generator import ( generate_ml_charts, generate_model_comparison_chart, generate_correlation_heatmap, generate_distribution_grid, generate_boxplot_grid, detect_stock_data, generate_stock_charts ) # 📈 DETECT STOCK/FINANCIAL DATA AND GENERATE STOCK CHARTS stock_info = detect_stock_data(df_clean) if stock_info['is_stock_data']: logger.info("📈 Stock/Financial data detected! Generating stock-specific charts...") stock_charts = generate_stock_charts(df_clean, stock_info, target_col) charts.update(stock_charts) logger.info(f" 📊 Generated {len(stock_charts)} stock charts") class_names = self.target_encoder.classes_.tolist() if self.target_encoder else None ml_charts = generate_ml_charts( task_type=self.task_type, y_test=y_test, y_pred=self._y_pred, y_proba=y_proba, feature_importance=self._get_importance(self.model), class_names=class_names, model_name=trainer.best_name ) charts.update(ml_charts) comparison_chart = generate_model_comparison_chart(trainer.results) if comparison_chart: charts['model_comparison'] = comparison_chart # 🆕 ADD EXTRA CHARTS FROM REAL DATA # Correlation Heatmap (numeric features only) numeric_df = df_clean.select_dtypes(include=[np.number]) if len(numeric_df.columns) >= 2: corr_chart = generate_correlation_heatmap(numeric_df, "Feature Correlations") if corr_chart: charts['correlation_heatmap'] = corr_chart # Distribution Grid (histograms of features) if len(numeric_df.columns) >= 1: dist_chart = generate_distribution_grid(numeric_df, "Feature Distributions") if dist_chart: charts['distribution_grid'] = dist_chart # Box Plot Grid if len(numeric_df.columns) >= 1: box_chart = generate_boxplot_grid(numeric_df, "Feature Box Plots") if box_chart: charts['boxplot_grid'] = box_chart # 🆕 ULTRA MODE: Premium Enterprise Charts if mode == 'ultra': try: from ml.chart_generator import generate_ultra_charts ultra_charts = generate_ultra_charts( task_type=self.task_type, y_test=y_test, y_pred=self._y_pred, y_proba=y_proba, feature_importance=self._get_importance(self.model), leaderboard=trainer.results, model_name=trainer.best_name, X_test=X_test, cv_scores=None # Could add if available ) # Merge ultra charts with standard charts charts.update(ultra_charts) logger.info(f" 🎨 Ultra Charts Generated: {list(ultra_charts.keys())}") except Exception as ultra_err: logger.warning(f"⚠️ Ultra chart error: {ultra_err}") import traceback traceback.print_exc() print(f"📊 Generated {len(charts)} charts: {list(charts.keys())}") except Exception as chart_err: logger.warning(f"⚠️ Chart generation error: {chart_err}") # CRITICAL: Save model for predictions try: self._save(user_id, charts=charts) print(f"💾 Model saved for user: {user_id}") except Exception as save_err: logger.warning(f"⚠️ Model save error: {save_err}") # 🛡️ PRODUCTION INTELLIGENCE: Get reliability score and warnings reliability_score = 75 # Default validation_warnings = [] if best_result and 'reliability_score' in best_result: reliability_score = best_result.get('reliability_score', 75) for r in trainer.results: if r.get('warning'): validation_warnings.append(f"{r.get('name', 'Model')}: {r.get('warning')}") return TrainResult( success=True, task_type=self.task_type, target_column=target_col, # IMPORTANT: Use original columns (not engineered features) for Features tab feature_columns=engineer.original_columns, best_model_name=trainer.best_name, best_model_metrics=best_metrics, leaderboard=[{'name': r['name'], 'metrics': r['metrics']} for r in trainer.results], feature_importance=self._get_importance(self.model), y_test=y_test, y_pred=self._y_pred, y_proba=y_proba, feature_metadata=getattr(self, 'feature_metadata', []), n_rows=len(df), n_cols=len(df.columns), processing_time=elapsed, charts=charts, is_nlp_task=False, primary_text_col=None, cleaned_file_path=cleaned_file_path, reliability_score=reliability_score, validation_warnings=validation_warnings if validation_warnings else None ) def production_train_selected( self, df: pd.DataFrame, target_col: Optional[str] = None, user_id: str = "default", selected_algorithms: List[str] = None ) -> 'TrainResult': """ 🎯 TRAIN ONLY USER-SELECTED ALGORITHMS Instead of training all fast/ultra models, train only the specific algorithms the user selected in the UI. Args: df: Input dataframe target_col: Target column name user_id: User ID for model storage selected_algorithms: List of algorithm keys like ['random_forest', 'xgboost', 'lightgbm'] Returns: TrainResult with best model from selected algorithms """ from sklearn.model_selection import train_test_split if not selected_algorithms: # Fallback to fast mode if no algorithms selected return self.production_train(df, target_col, user_id, mode='fast') # Reset cancellation flag CANCELLATION_FLAGS[user_id] = False cleaned_file_path = None self.errors = [] start = datetime.now() logger.info("=" * 60) logger.info("🎯 TRAINING USER-SELECTED ALGORITHMS") logger.info(f" Algorithms: {selected_algorithms}") logger.info("=" * 60) # Detect target if not provided if not target_col: target_col = self._detect_target(df) self.target_column = target_col logger.info(f"🎯 Target: {target_col}") logger.info(f"📊 Data: {df.shape[0]} rows, {df.shape[1]} columns") # 1. Clean data cleaner = ProductionDataCleaner() df_clean = cleaner.clean(df, target_col) # Drop NaN targets df_clean = df_clean.dropna(subset=[target_col]) # Save cleaned data try: from utils.paths import get_user_paths user_paths = get_user_paths(user_id) upload_dir = user_paths['files'] cleaned_filename = f"cleaned_{int(datetime.now().timestamp())}.csv" cleaned_full_path = upload_dir / cleaned_filename df_clean.to_csv(cleaned_full_path, index=False) cleaned_file_path = cleaned_filename except Exception as e: logger.error(f"Failed to save cleaned data: {e}") # 2. Detect task type y_temp = df_clean[target_col] unique_ratio = len(y_temp.unique()) / len(y_temp) if y_temp.dtype == 'object' or (y_temp.dtype in ['int64', 'float64'] and len(y_temp.unique()) <= 20): self.task_type = 'classification' self.task_type_simple = 'classification' else: self.task_type = 'regression' self.task_type_simple = 'regression' logger.info(f"🔍 Task Type: {self.task_type}") # 3. CRITICAL: Encode target for classification BEFORE feature engineering # This converts string labels like ['No', 'Yes'] to numeric [0, 1] # which is required by all ML algorithms (XGBoost, LightGBM, etc.) if self.task_type_simple == 'classification': self.target_encoder = LabelEncoder() df_clean[target_col] = self.target_encoder.fit_transform(df_clean[target_col].astype(str)) self.n_classes = len(self.target_encoder.classes_) logger.info(f" ✅ Target encoded: {list(self.target_encoder.classes_)} → {list(range(self.n_classes))}") # 4. Feature engineering engineer = ProductionFeatureEngineer() X, y, feature_names = engineer.fit_transform(df_clean, target_col, self.task_type_simple) # CRITICAL: Ensure y is numeric for classification (int) or regression (float) if self.task_type_simple == 'classification': y = y.astype(int) else: y = y.astype(float) # Calculate feature metadata for Playground tab self._calculate_feature_metadata(df_clean.drop(columns=[target_col])) # 5. Train/test split if self.task_type_simple == 'classification': try: X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) except ValueError: X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) else: X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) logger.info(f" Train: {len(X_train)} | Test: {len(X_test)}") # 5. Build ONLY the selected models from ml.production_ml_core import build_selected_models models = build_selected_models( selected_algorithms, self.task_type_simple, n_samples=len(X_train) ) logger.info(f"🔧 Training {len(models)} selected models: {list(models.keys())}") # 6. Train each model results = [] best_model = None best_name = None best_score = -np.inf for name, model in models.items(): try: check_cancellation(user_id) logger.info(f" Training {name}...") model.fit(X_train, y_train) if self.task_type_simple == 'classification': score = model.score(X_test, y_test) y_pred_temp = model.predict(X_test) from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score metrics = { 'accuracy': accuracy_score(y_test, y_pred_temp), 'f1': f1_score(y_test, y_pred_temp, average='weighted', zero_division=0), 'precision': precision_score(y_test, y_pred_temp, average='weighted', zero_division=0), 'recall': recall_score(y_test, y_pred_temp, average='weighted', zero_division=0) } # Add ROC-AUC try: n_classes = len(np.unique(y_test)) if n_classes == 2 and hasattr(model, 'predict_proba'): y_proba_temp = model.predict_proba(X_test)[:, 1] metrics['roc_auc'] = float(roc_auc_score(y_test, y_proba_temp)) elif n_classes > 2 and hasattr(model, 'predict_proba'): y_proba_temp = model.predict_proba(X_test) metrics['roc_auc'] = float(roc_auc_score( y_test, y_proba_temp, multi_class='ovr', average='weighted' )) except Exception: pass else: from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error y_pred_temp = model.predict(X_test) score = r2_score(y_test, y_pred_temp) metrics = { 'r2': score, 'mse': float(mean_squared_error(y_test, y_pred_temp)), 'rmse': np.sqrt(mean_squared_error(y_test, y_pred_temp)), 'mae': mean_absolute_error(y_test, y_pred_temp) } results.append({ 'name': name, 'model': model, 'score': score, 'metrics': metrics }) if score > best_score: best_score = score best_model = model best_name = name logger.info(f" ✅ {name}: {score:.4f}") except Exception as e: logger.warning(f" ❌ {name} failed: {e}") if not best_model: # Return a proper TrainResult with empty/default values elapsed = (datetime.now() - start).total_seconds() return TrainResult( success=False, task_type=self.task_type or 'classification', target_column=target_col or '', feature_columns=[], best_model_name='None', best_model_metrics={}, leaderboard=[], feature_importance=[], y_test=np.array([]), y_pred=np.array([]), y_proba=None, feature_metadata=[], n_rows=len(df), n_cols=len(df.columns), processing_time=elapsed, charts={}, is_nlp_task=False, primary_text_col=None, cleaned_file_path=None, reliability_score=0, # 🛡️ Failed training = 0 reliability validation_warnings=['Training failed - no model could be trained'] ) # Store results self.model = best_model self.model_name = best_name self._y_test = y_test self._y_pred = best_model.predict(X_test) self.production_engineer = engineer self.production_mode = True self.feature_columns = feature_names # Store for external access # Get probabilities if classification y_proba = None if self.task_type_simple == 'classification' and hasattr(best_model, 'predict_proba'): try: y_proba = best_model.predict_proba(X_test) except: pass self._y_proba = y_proba # Build best metrics FROM RESULTS (not empty) best_metrics = {} for r in results: if r['name'] == best_name: best_metrics = r['metrics'] break # IMPORTANT: Store metrics on self so they're saved to persistence self.metrics = best_metrics elapsed = (datetime.now() - start).total_seconds() logger.info(f"⏱️ Training completed in {elapsed:.1f}s") # Generate charts - SAME AS production_train charts = {} try: from ml.chart_generator import ( generate_ml_charts, generate_model_comparison_chart, generate_correlation_heatmap, generate_distribution_grid, generate_boxplot_grid, detect_stock_data, generate_stock_charts ) # 📈 DETECT STOCK/FINANCIAL DATA AND GENERATE STOCK CHARTS stock_info = detect_stock_data(df_clean) if stock_info['is_stock_data']: logger.info("📈 Stock/Financial data detected! Generating stock-specific charts...") stock_charts = generate_stock_charts(df_clean, stock_info, target_col) charts.update(stock_charts) logger.info(f" 📊 Generated {len(stock_charts)} stock charts") # Get class names for classification class_names = None if self.task_type_simple == 'classification': if hasattr(self, 'target_encoder') and self.target_encoder is not None: class_names = list(self.target_encoder.classes_) else: class_names = list(np.unique(y_test)) charts.update(generate_ml_charts( task_type=self.task_type, y_test=y_test, y_pred=self._y_pred, y_proba=y_proba, feature_importance=self._get_importance(best_model), class_names=class_names, model_name=best_name )) # Model comparison chart comparison_chart = generate_model_comparison_chart(results) if comparison_chart: charts['model_comparison'] = comparison_chart # Correlation Heatmap numeric_df = df_clean.select_dtypes(include=[np.number]) if len(numeric_df.columns) >= 2: corr_chart = generate_correlation_heatmap(numeric_df, "Feature Correlations") if corr_chart: charts['correlation_heatmap'] = corr_chart # Distribution Grid if len(numeric_df.columns) >= 1: dist_chart = generate_distribution_grid(numeric_df, "Feature Distributions") if dist_chart: charts['distribution_grid'] = dist_chart # Box Plot Grid if len(numeric_df.columns) >= 1: box_chart = generate_boxplot_grid(numeric_df, "Feature Box Plots") if box_chart: charts['boxplot_grid'] = box_chart logger.info(f"📊 Generated {len(charts)} charts: {list(charts.keys())}") except Exception as e: logger.warning(f"Chart generation error: {e}") import traceback traceback.print_exc() # Save model try: self._save(user_id, charts=charts) except Exception as e: logger.warning(f"Model save error: {e}") # 🛡️ PRODUCTION INTELLIGENCE: Compute reliability from results reliability_score = 75 # Default validation_warnings = [] for r in results: if r.get('reliability_score'): if r.get('name') == best_name: reliability_score = r.get('reliability_score') if r.get('warning'): validation_warnings.append(f"{r.get('name', 'Model')}: {r.get('warning')}") return TrainResult( success=True, task_type=self.task_type, target_column=target_col, feature_columns=engineer.original_columns, best_model_name=best_name, best_model_metrics=best_metrics, leaderboard=[{'name': r['name'], 'metrics': r['metrics'], 'score': r['score']} for r in results], feature_importance=self._get_importance(best_model), y_test=y_test, y_pred=self._y_pred, y_proba=y_proba, feature_metadata=getattr(self, 'feature_metadata', []), n_rows=len(df), n_cols=len(df.columns), processing_time=elapsed, charts=charts, is_nlp_task=False, primary_text_col=None, cleaned_file_path=cleaned_file_path, reliability_score=reliability_score, validation_warnings=validation_warnings if validation_warnings else None ) async def train_with_test_set( self, train_df: pd.DataFrame, test_df: pd.DataFrame, target_col: Optional[str] = None, user_id: str = "default" ) -> 'TrainResult': """ Train with SEPARATE train and test datasets. Use this when you have pre-split data (e.g., Kaggle competitions). """ self.errors = [] start = datetime.now() logger.info("=" * 60) logger.info("🚀 PRODUCTION ML ENGINE v7.0 - TRAIN WITH TEST SET") logger.info("=" * 60) logger.info(f"📊 Train: {len(train_df)} rows | Test: {len(test_df)} rows") # Detect target if not target_col: target_col = self._detect_target(train_df) else: logger.info(f"🎯 Target: {target_col}") # Analyze data profile self._analyze_data_profile(train_df, target_col) # Preprocess TRAIN data (fit transformers) X_train, y_train = self._preprocess_training(train_df, target_col) logger.info(f" Train shape: {X_train.shape}") # Preprocess TEST data (use fitted transformers) # Remove target from test df for preprocessing test_features = test_df.drop(columns=[target_col]) y_test_raw = test_df[target_col] # Process each test row using single prediction preprocessor X_test_parts = [] for idx, row in test_features.iterrows(): try: x_single = self._preprocess_single(row.to_dict()) X_test_parts.append(x_single) except Exception as e: logger.warning(f" ⚠️ Test row {idx} error: {e}") # Append zeros matching train shape X_test_parts.append(np.zeros((1, X_train.shape[1]))) X_test = np.vstack(X_test_parts) # Process y_test if self.task_type_simple == 'classification': y_test = self.target_encoder.transform(y_test_raw.fillna('_MISSING_').astype(str).str.strip()) else: y_test = pd.to_numeric(y_test_raw, errors='coerce').fillna(0).values.astype(float) logger.info(f" Test shape: {X_test.shape}") # Store for charts self._X_train = X_train self._X_test = X_test # Continue with normal training flow (no SMOTE for now, user has balanced data) models = self._get_adaptive_models() results = [] best_score = -np.inf best_model = None best_name = None best_pred = None best_proba = None scoring = 'f1_weighted' if self.task_type_simple == 'classification' else 'r2' # CV folds cv_folds = 5 if len(train_df) > 500 else 3 n_iter = 8 logger.info(f"🤖 Training {len(models)} models on user-provided train/test split...") trained_models = {} for idx, (name, (model, params)) in enumerate(models.items(), 1): try: t0 = datetime.now() logger.info(f" [{idx}/{len(models)}] {name}...") # Simple fit (skip CV search for speed with user test set) model.fit(X_train, y_train) trained_models[name] = model y_pred = model.predict(X_test) y_proba = None if hasattr(model, 'predict_proba'): try: y_proba = model.predict_proba(X_test) except: pass # Metrics if self.task_type_simple == 'classification': score = f1_score(y_test, y_pred, average='weighted', zero_division=0) acc = accuracy_score(y_test, y_pred) metrics = {'f1': round(score, 4), 'accuracy': round(acc, 4)} else: score = r2_score(y_test, y_pred) mae = mean_absolute_error(y_test, y_pred) metrics = {'r2': round(score, 4), 'mae': round(mae, 4)} elapsed = (datetime.now() - t0).total_seconds() results.append({'name': name, 'metrics': metrics, 'training_time': round(elapsed, 2)}) metric_name = 'f1' if self.task_type_simple == 'classification' else 'r2' logger.info(f"{metric_name}={score:.3f} ({elapsed:.1f}s)") if score > best_score: best_score = score best_model = model best_name = name best_pred = y_pred best_proba = y_proba except Exception as e: logger.warning(f"ERROR - {name}: {str(e)[:50]}") self.errors.append(f"{name}: {str(e)[:100]}") self.model = best_model self.model_name = best_name if self.model is None: raise ValueError("All models failed. Errors: " + "; ".join(self.errors[-3:])) logger.info(f"🏆 Best: {best_name} (score={best_score:.3f})") # Save model self._y_test = y_test self._y_pred = best_pred self._y_proba = best_proba self._save(user_id) elapsed = (datetime.now() - start).total_seconds() logger.info(f"✅ Complete in {elapsed:.1f}s") # IMPORTANT: Store metrics on self so they're saved to persistence self.metrics = results[0]['metrics'] if results else {} # Generate charts charts = {} try: from ml.chart_generator import generate_ml_charts, generate_model_comparison_chart class_names = self.target_encoder.classes_.tolist() if self.target_encoder else None charts = generate_ml_charts( task_type=self.task_type, y_test=y_test, y_pred=best_pred, y_proba=best_proba, feature_importance=self._get_importance(best_model), class_names=class_names, model_name=best_name ) comparison_chart = generate_model_comparison_chart(results) if comparison_chart: charts['model_comparison'] = comparison_chart except Exception as chart_err: logger.warning(f"⚠️ Chart generation error: {chart_err}") # CRITICAL: Save model for predictions try: self._save(user_id) print(f"💾 Model saved for user: {user_id}") except Exception as save_err: logger.warning(f"⚠️ Model save error: {save_err}") # 🛡️ PRODUCTION INTELLIGENCE: Compute reliability reliability_score = 75 validation_warnings = [] for r in results: if r.get('name') == best_name and r.get('reliability_score'): reliability_score = r.get('reliability_score') if r.get('warning'): validation_warnings.append(f"{r.get('name', 'Model')}: {r.get('warning')}") return TrainResult( success=True, task_type=self.task_type, target_column=target_col, feature_columns=self.feature_columns, best_model_name=best_name, best_model_metrics=results[0]['metrics'] if results else {}, leaderboard=results, feature_importance=self._get_importance(best_model), y_test=y_test, y_pred=best_pred, y_proba=best_proba, feature_metadata=self.feature_metadata, n_rows=len(train_df) + len(test_df), n_cols=len(train_df.columns), processing_time=elapsed, charts=charts, is_nlp_task=self.is_nlp_task, primary_text_col=self.primary_text_col, reliability_score=reliability_score, validation_warnings=validation_warnings if validation_warnings else None ) async def train(self, df: pd.DataFrame, target_col: Optional[str] = None, user_id: str = "default") -> 'TrainResult': """PRODUCTION-LEVEL Main training pipeline with adaptive technique selection""" self.errors = [] start = datetime.now() logger.info("=" * 60) logger.info("🚀 PRODUCTION ML ENGINE v7.0 - COMPLETE AUTOML PIPELINE") logger.info("=" * 60) logger.info(f"📊 Data: {len(df)} rows, {len(df.columns)} columns") # Detect target if not target_col: target_col = self._detect_target(df) else: logger.info(f"🎯 Target (user specified): {target_col}") # ADAPTIVE: Analyze data profile BEFORE preprocessing to recommend techniques self._analyze_data_profile(df, target_col) # Preprocess X, y = self._preprocess_training(df, target_col) # Split (stratified for classification) stratify = y if self.task_type_simple == 'classification' and self.n_classes < 100 else None X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=stratify ) logger.info(f" Train: {len(X_train)}, Test: {len(X_test)}") # Store for learning curves and chart generation self._X_train = X_train self._X_test = X_test # Apply SMOTE if recommended by data profile if 'smote' in self.data_profile.get('recommended_techniques', []): X_train_balanced, y_train_balanced = self._handle_class_imbalance(X_train, y_train) else: X_train_balanced, y_train_balanced = X_train, y_train # ADAPTIVE: Get models based on data profile (not hardcoded) models = self._get_adaptive_models() results = [] best_score = -np.inf best_model = None best_name = None best_pred = None best_proba = None scoring = 'f1_weighted' if self.task_type_simple == 'classification' else 'r2' # ADAPTIVE: CV folds based on data profile profile = self.data_profile if profile.get('is_small_data'): cv_folds = 10 n_iter = 10 elif profile.get('is_large_data'): cv_folds = 3 n_iter = 5 else: cv_folds = 5 n_iter = 8 logger.info(f"🤖 Training {len(models)} ADAPTIVE models ({cv_folds}-fold CV)...") # Track trained models for stacking trained_models = {} for idx, (name, (model, params)) in enumerate(models.items(), 1): try: t0 = datetime.now() logger.info(f" [{idx}/{len(models)}] {name}...") # Fallback mechanism: Try GridSearch/RandomSearch first, then simple fit try: search = RandomizedSearchCV( model, params, n_iter=min(n_iter, np.prod([len(v) for v in params.values()])), cv=self._get_cv_strategy(y_train_balanced, n_splits=cv_folds), # FIX: Use correct CV strategy scoring=scoring, n_jobs=1, random_state=42, error_score='raise' ) search.fit(X_train_balanced, y_train_balanced) best_est = search.best_estimator_ best_params = search.best_params_ except Exception as search_err: logger.warning(f" ⚠️ Search failed ({str(search_err)[:50]}), falling back to simple fit...") model.fit(X_train_balanced, y_train_balanced) best_est = model best_params = "default" # Store trained model for stacking trained_models[name] = best_est # Predict y_pred = best_est.predict(X_test) y_proba = None if hasattr(best_est, 'predict_proba'): try: y_proba = best_est.predict_proba(X_test) except: pass # Metrics if self.task_type_simple == 'classification': score = f1_score(y_test, y_pred, average='weighted', zero_division=0) acc = accuracy_score(y_test, y_pred) prec = precision_score(y_test, y_pred, average='weighted', zero_division=0) rec = recall_score(y_test, y_pred, average='weighted', zero_division=0) metrics = { 'f1': round(score, 4), 'accuracy': round(acc, 4), 'precision': round(prec, 4), 'recall': round(rec, 4) } # Add ROC-AUC try: n_classes = len(np.unique(y_test)) if n_classes == 2 and y_proba is not None: metrics['roc_auc'] = round(float(roc_auc_score(y_test, y_proba[:, 1])), 4) elif n_classes > 2 and y_proba is not None: metrics['roc_auc'] = round(float(roc_auc_score( y_test, y_proba, multi_class='ovr', average='weighted' )), 4) except Exception: pass else: score = r2_score(y_test, y_pred) mae = mean_absolute_error(y_test, y_pred) mse = mean_squared_error(y_test, y_pred) metrics = { 'r2': round(score, 4), 'mse': round(mse, 4), 'rmse': round(float(np.sqrt(mse)), 4), 'mae': round(mae, 4) } elapsed = (datetime.now() - t0).total_seconds() results.append({ 'name': name, 'metrics': metrics, 'training_time': round(elapsed, 2), 'importance': self._get_importance(best_est), 'best_params': best_params }) metric_name = 'f1' if self.task_type_simple == 'classification' else 'r2' logger.info(f"{metric_name}={score:.3f} ({elapsed:.1f}s)") if score > best_score: best_score = score best_model = best_est best_name = name best_pred = y_pred best_proba = y_proba except Exception as e: import traceback traceback.print_exc() error_msg = f"{name}: {str(e)[:100]}" logger.warning(f"ERROR - {error_msg}") self.errors.append(error_msg) # Store error for reporting self.model = best_model self.model_name = best_name if self.model is None: error_summary = "; ".join(self.errors[-3:]) if hasattr(self, 'errors') else "Unknown error" raise ValueError(f"All models failed to train. Errors: {error_summary}") logger.info(f"🏆 Best: {best_name} (score={best_score:.3f})") # Retrain on full data try: logger.info(f"🔄 Retraining {best_name} on full data...") self.model.fit(X, y) except Exception as e: logger.warning(f"⚠️ Retraining failed, keeping split model: {e}") # Keep the already trained best_model from split pass logger.info(f" ✅ Retrained on {len(X)} samples") # === STACKING ENSEMBLE === # Try to build an ensemble of the best models for top 1% performance if len(trained_models) >= 2 and results: try: # Sort first to get top models temp_results = sorted(results, key=lambda x: x['metrics'].get(metric_name, 0), reverse=True) top_models = {r['name']: trained_models[r['name']] for r in temp_results[:3]} stacker = self._build_stacking_ensemble(top_models, X_train, y_train) if stacker: y_pred_stack = stacker.predict(X_test) if self.task_type_simple == 'classification': stack_score = f1_score(y_test, y_pred_stack, average='weighted', zero_division=0) stack_acc = accuracy_score(y_test, y_pred_stack) stack_metrics = {'f1': round(stack_score, 4), 'accuracy': round(stack_acc, 4)} try: y_proba_stack = stacker.predict_proba(X_test) except: y_proba_stack = None else: stack_score = r2_score(y_test, y_pred_stack) stack_mae = mean_absolute_error(y_test, y_pred_stack) stack_metrics = {'r2': round(stack_score, 4), 'mae': round(stack_mae, 4)} y_proba_stack = None logger.info(f" 🤖 Stacking Ensemble: {metric_name}={stack_score:.3f}") # Add to results results.append({ 'name': 'StackingEnsemble', 'metrics': stack_metrics, 'training_time': 5.0, # Approximate 'importance': [], # Stacking doesn't easily support feature importance 'best_params': {'estimators': list(top_models.keys())} }) # If better, update best if stack_score > best_score: logger.info(f" 🚀 Stacking Ensemble is the new BEST model! (+{(stack_score - best_score):.4f})") best_score = stack_score best_model = stacker best_name = 'StackingEnsemble' best_pred = y_pred_stack best_proba = y_proba_stack # Add to trained models so it can be saved/retrained trained_models['StackingEnsemble'] = stacker except Exception as e: logger.warning(f" ⚠️ Stacking evaluation failed: {e}") # Sort results metric_key = 'f1' if self.task_type_simple == 'classification' else 'r2' results.sort(key=lambda x: x['metrics'].get(metric_key, 0), reverse=True) # CRITICAL: Store training metrics on self BEFORE saving # These are needed for get_model_metrics() to return real values self._y_test = y_test self._y_pred = best_pred self._y_proba = best_proba self.metrics = results[0]['metrics'] if results else {} # Calculate and store confusion matrix for classification if self.task_type_simple == 'classification': try: # Use global imports instead of local re-import to avoid UnboundLocalError self.confusion_matrix = confusion_matrix(y_test, best_pred) # Also store full metrics self.metrics['accuracy'] = float(accuracy_score(y_test, best_pred)) self.metrics['f1'] = float(f1_score(y_test, best_pred, average='weighted', zero_division=0)) self.metrics['precision'] = float(precision_score(y_test, best_pred, average='weighted', zero_division=0)) self.metrics['recall'] = float(recall_score(y_test, best_pred, average='weighted', zero_division=0)) logger.info(f"📊 Stored metrics: Accuracy={self.metrics['accuracy']:.1%}, F1={self.metrics['f1']:.1%}") except Exception as cm_err: logger.warning(f"⚠️ Confusion matrix error: {cm_err}") self.confusion_matrix = None else: # Regression metrics try: # Use global imports to avoid UnboundLocalError self.metrics['r2'] = float(r2_score(y_test, best_pred)) self.metrics['mae'] = float(mean_absolute_error(y_test, best_pred)) self.metrics['rmse'] = float(np.sqrt(mean_squared_error(y_test, best_pred))) logger.info(f"📊 Stored metrics: R²={self.metrics['r2']:.3f}, MAE={self.metrics['mae']:.2f}") except Exception as reg_err: logger.warning(f"⚠️ Regression metrics error: {reg_err}") # Save (now includes metrics, y_test, y_pred, confusion_matrix) self._save(user_id) # Verify self._verify(df.head(5), target_col) processing_time = (datetime.now() - start).total_seconds() logger.info("=" * 60) logger.info(f"✅ Complete in {processing_time:.1f}s") logger.info("=" * 60) # Generate all charts using the production chart generator try: from ml.chart_generator import ( generate_ml_charts, generate_model_comparison_chart, generate_correlation_heatmap, generate_distribution_grid, generate_boxplot_grid ) # Get class names for classification class_names = None if self.target_encoder is not None: class_names = self.target_encoder.classes_.tolist() # Generate ML charts charts = generate_ml_charts( task_type=self.task_type, y_test=y_test, y_pred=best_pred, y_proba=best_proba, feature_importance=self._get_importance(self.model), class_names=class_names, model_name=best_name ) # Add model comparison chart comparison_chart = generate_model_comparison_chart(results) if comparison_chart: charts['model_comparison'] = comparison_chart # 🆕 ADD EXTRA CHARTS FROM REAL DATA # Correlation Heatmap (numeric features only) numeric_df = df.select_dtypes(include=[np.number]) if len(numeric_df.columns) >= 2: corr_chart = generate_correlation_heatmap(numeric_df, "Feature Correlations") if corr_chart: charts['correlation_heatmap'] = corr_chart # Distribution Grid (histograms of features) if len(numeric_df.columns) >= 1: dist_chart = generate_distribution_grid(numeric_df, "Feature Distributions") if dist_chart: charts['distribution_grid'] = dist_chart # Box Plot Grid if len(numeric_df.columns) >= 1: box_chart = generate_boxplot_grid(numeric_df, "Feature Box Plots") if box_chart: charts['boxplot_grid'] = box_chart logger.info(f"📊 Generated {len(charts)} charts: {list(charts.keys())}") except Exception as chart_err: logger.warning(f"⚠️ Chart generation error: {chart_err}") import traceback traceback.print_exc() charts = {} # IMPORTANT: Store metrics on self so they're saved to persistence self.metrics = results[0]['metrics'] if results else {} # CRITICAL: Save model for predictions try: self._save(user_id) print(f"💾 Model saved for user: {user_id}") except Exception as save_err: logger.warning(f"⚠️ Model save error: {save_err}") # 🛡️ PRODUCTION INTELLIGENCE: Compute reliability reliability_score = 75 validation_warnings = [] for r in results: if r.get('name') == best_name and r.get('reliability_score'): reliability_score = r.get('reliability_score') if r.get('warning'): validation_warnings.append(f"{r.get('name', 'Model')}: {r.get('warning')}") return TrainResult( success=True, task_type=self.task_type, target_column=target_col, feature_columns=self.feature_columns, best_model_name=best_name, best_model_metrics=results[0]['metrics'] if results else {}, leaderboard=results, feature_importance=results[0].get('importance', []) if results else [], y_test=y_test, y_pred=best_pred, y_proba=best_proba[:, 1] if best_proba is not None and self.task_type_simple == 'classification' and len(best_proba.shape) > 1 and best_proba.shape[1] == 2 else None, feature_metadata=self.feature_metadata, n_rows=len(df), n_cols=len(df.columns), processing_time=processing_time, charts=charts, is_nlp_task=getattr(self, 'is_nlp_task', False), primary_text_col=getattr(self, 'primary_text_col', None), reliability_score=reliability_score, validation_warnings=validation_warnings if validation_warnings else None ) def _get_importance(self, model) -> List[Dict]: """Get feature importance with REAL column names for charts Uses feature_metadata to properly map engineered features back to their source columns. """ values = [] # Get importance values from model if hasattr(model, 'estimators_'): all_importances = [] for est in model.estimators_: if hasattr(est, 'feature_importances_'): all_importances.append(est.feature_importances_) elif hasattr(est, 'coef_'): all_importances.append(np.abs(est.coef_).flatten()) if all_importances: max_len = max(len(imp) for imp in all_importances) padded = [np.pad(imp, (0, max_len - len(imp))) for imp in all_importances] values = np.mean(padded, axis=0) if len(values) == 0: if hasattr(model, 'feature_importances_'): values = model.feature_importances_ elif hasattr(model, 'coef_'): values = np.abs(model.coef_).flatten() elif hasattr(model, 'steps'): return self._get_importance(model.steps[-1][1]) if len(values) == 0: # No importance available, use equal distribution for known columns all_cols = getattr(self, 'numeric_cols', []) + getattr(self, 'categorical_cols', []) + getattr(self, 'text_cols', []) if all_cols: return [{'feature': col, 'importance': round(1.0 / len(all_cols), 4), 'rank': i + 1} for i, col in enumerate(all_cols)] return [] # Normalize values = np.array(values) if values.sum() > 0: values = values / values.sum() n_features = len(values) # ================================================================== # USE FEATURE_METADATA FOR PROPER COLUMN MAPPING # ================================================================== column_importance = {} idx = 0 # feature_metadata is built during preprocessing in order: # 1. numeric columns (each adds 1 feature) # 2. categorical columns (each adds 1 feature) # 3. text columns (each adds vocab_size or n_features) if hasattr(self, 'feature_metadata') and self.feature_metadata: for meta in self.feature_metadata: col_name = meta.get('name', 'Unknown') col_type = meta.get('type', 'numeric') if col_type == 'numeric': # Numeric: 1 feature if idx < n_features: column_importance[col_name] = float(values[idx]) idx += 1 elif col_type == 'categorical': # Categorical: 1 feature (label encoded) if idx < n_features: column_importance[col_name] = float(values[idx]) idx += 1 elif col_type in ('text', 'nlp_text'): # Text: many TF-IDF features - aggregate them n_text_features = meta.get('vocab_size', meta.get('n_features', 1)) text_imp = 0.0 for _ in range(n_text_features): if idx < n_features: text_imp += float(values[idx]) idx += 1 column_importance[col_name] = text_imp # Fallback if feature_metadata is empty or doesn't work if not column_importance or sum(column_importance.values()) < 0.01: # Use original columns directly all_cols = getattr(self, 'numeric_cols', []) + getattr(self, 'categorical_cols', []) + getattr(self, 'text_cols', []) if all_cols: # Distribute all importance across source columns per_col = 1.0 / len(all_cols) if all_cols else 0 for col in all_cols: column_importance[col] = per_col # Re-normalize total = sum(column_importance.values()) if total > 0: column_importance = {k: v / total for k, v in column_importance.items()} # Convert to sorted list importance = [] for rank, (col, imp) in enumerate(sorted(column_importance.items(), key=lambda x: x[1], reverse=True), 1): if rank > 15: break importance.append({ 'feature': col, 'importance': round(float(imp), 4), 'rank': rank }) return importance def _verify(self, df: pd.DataFrame, target_col: str): """Verify predictions""" print("\n📋 Verification:") for i in range(min(5, len(df))): row = df.iloc[i] actual = row[target_col] data = {col: row[col] for col in self.feature_columns if col in row.index} try: pred = self.predict(data) p = pred['prediction'] if self.task_type_simple == 'regression': try: err = abs(float(p) - float(actual)) / abs(float(actual)) * 100 if float(actual) != 0 else 0 print(f" Row {i}: Actual={actual:.2f}, Pred={float(p):.2f}, Error={err:.1f}%") except: print(f" Row {i}: Actual={actual}, Pred={p}") else: match = "✅" if str(p) == str(actual) else "❌" print(f" Row {i}: Actual={actual}, Pred={p} {match}") except Exception as e: print(f" Row {i}: ERROR - {e}") def predict(self, data: Dict[str, Any]) -> Dict[str, Any]: """Make prediction with comprehensive logging""" if self.model is None: raise ValueError("No model trained") logger.info(f"🔮 Prediction request with {len(data)} features") logger.info(f" Input features: {list(data.keys())}") logger.info(f" Production mode: {getattr(self, 'production_mode', False)}") logger.info(f" Has production_engineer: {hasattr(self, 'production_engineer') and self.production_engineer is not None}") # Check if using production pipeline if getattr(self, 'production_mode', False) and hasattr(self, 'production_engineer') and self.production_engineer is not None: logger.info(" Using PRODUCTION pipeline") X = self._preprocess_single_production(data) else: logger.info(" Using LEGACY pipeline") X = self._preprocess_single(data) logger.info(f" Preprocessed X shape: {X.shape}") pred = self.model.predict(X)[0] logger.info(f" Raw prediction: {pred}") if self.target_encoder: try: original_pred = pred pred = self.target_encoder.inverse_transform([int(pred)])[0] logger.info(f" Decoded prediction: {original_pred} -> {pred}") except Exception as e: logger.warning(f" Target decoder failed: {e}") prob = None conf = None if hasattr(self.model, 'predict_proba'): try: proba = self.model.predict_proba(X)[0] prob = [float(p) for p in proba] conf = float(max(proba)) logger.info(f" Confidence: {conf:.2%}") except: pass logger.info(f"✅ Final prediction: {pred}") return {'prediction': str(pred), 'probability': prob, 'confidence': conf, 'model': self.model_name} def _preprocess_single_production(self, data: Dict[str, Any]) -> np.ndarray: """ Preprocess single input using Production Feature Engineer Used when model was trained with production_train UNIFIED: Delegates to ProductionFeatureEngineer.transform_single to ensure prediction features EXACTLY match training features. """ if not hasattr(self, 'production_engineer'): raise ValueError("No production engineer found. Model may have been trained with legacy pipeline.") # Use the unified transform method return self.production_engineer.transform_single(data) def _save(self, user_id: str, charts: Optional[Dict[str, str]] = None): """Save model and preprocessors with enhanced persistence""" save_dir = os.path.join(STORAGE_PATH, user_id) os.makedirs(save_dir, exist_ok=True) data = { 'model': self.model, 'model_name': self.model_name, 'task_type': self.task_type, 'task_type_simple': self.task_type_simple, 'n_classes': self.n_classes, 'target_column': self.target_column, 'feature_columns': self.feature_columns, 'numeric_cols': self.numeric_cols, 'categorical_cols': self.categorical_cols, 'text_cols': self.text_cols, 'label_encoders': self.label_encoders, 'target_encoder': self.target_encoder, 'text_vectorizers': self.text_vectorizers, 'scaler': self.scaler, 'numeric_fill_values': self.numeric_fill_values, 'feature_metadata': self.feature_metadata, 'metrics': getattr(self, 'metrics', {}), 'confusion_matrix': getattr(self, 'confusion_matrix', None), 'y_test': getattr(self, '_y_test', None), 'y_pred': getattr(self, '_y_pred', None), 'y_proba': getattr(self, '_y_proba', None), # NEW: Save NLP and Advanced Feature Engineering state 'is_nlp_task': getattr(self, 'is_nlp_task', False), 'primary_text_col': getattr(self, 'primary_text_col', None), 'nlp_scaler': getattr(self, 'nlp_scaler', None), 'feature_engineer': self.feature_engineer, # PRODUCTION PIPELINE 'production_mode': getattr(self, 'production_mode', False), 'production_engineer': getattr(self, 'production_engineer', None), } with open(os.path.join(save_dir, "model.pkl"), 'wb') as f: pickle.dump(data, f) # Also save via the new persistence manager for versioning & metadata try: from ml.model_persistence import model_persistence model_persistence.save_model( user_id=user_id, engine_state=data, model_name=getattr(self, 'model_name', 'Unknown'), task_type=self.task_type, target_column=self.target_column, feature_columns=self.feature_columns, metrics=getattr(self, 'metrics', {}), dataset_info={ 'n_features': len(self.feature_columns), 'n_numeric': len(self.numeric_cols), 'n_categorical': len(self.categorical_cols), 'n_text': len(self.text_cols), 'is_nlp_task': getattr(self, 'is_nlp_task', False) } ) # Save charts if provided if charts: model_persistence.save_charts(user_id, charts) logger.info(f"📊 Saved {len(charts)} charts to persistence") logger.info(f"💾 Saved with versioning to model persistence manager") except Exception as e: logger.warning(f"⚠️ Persistence manager save failed: {e}") print(f"💾 Saved to {save_dir}") def load(self, user_id: str) -> bool: """Load model - tries model_persistence first, then legacy model.pkl""" # NEW: Try loading from model_persistence first (has latest trained model) try: from ml.model_persistence import get_model_persistence_manager pm = get_model_persistence_manager() result = pm.load_model(user_id) if result and result.get('model'): self.model = result['model'] self.model_name = result.get('model_name', 'Unknown') self.task_type = result.get('task_type', 'classification') self.task_type_simple = 'classification' if 'classification' in self.task_type else 'regression' self.feature_columns = result.get('feature_columns', []) self.target_column = result.get('target_column', '') self.feature_metadata = result.get('feature_metadata', []) self.metrics = result.get('metrics', {}) self.target_encoder = result.get('target_encoder') self.n_classes = result.get('n_classes', 2) # Load preprocessing objects self.label_encoders = result.get('label_encoders', {}) self.text_vectorizers = result.get('text_vectorizers', {}) self.scaler = result.get('scaler') self.production_engineer = result.get('production_engineer') # Load additional preprocessing attributes self.numeric_cols = result.get('numeric_cols', []) self.categorical_cols = result.get('categorical_cols', []) self.text_cols = result.get('text_cols', []) self.numeric_fill_values = result.get('numeric_fill_values', {}) # CRITICAL: Load y_test/y_pred for chart generation & evaluate self._y_test = result.get('y_test') self._y_pred = result.get('y_pred') self._y_proba = result.get('y_proba') self.confusion_matrix = result.get('confusion_matrix') # CRITICAL FIX: Use saved production_mode OR infer from production_engineer # Priority: saved value > inferred from production_engineer saved_production_mode = result.get('production_mode', None) if saved_production_mode is not None: self.production_mode = saved_production_mode else: self.production_mode = self.production_engineer is not None # Also load feature_engineer for legacy pipeline if 'feature_engineer' in result: self.feature_engineer = result['feature_engineer'] print(f"📂 Loaded {self.model_name} from model_persistence (latest)") print(f" Production mode: {self.production_mode}") print(f" Features: {len(self.feature_columns)}, Target: {self.target_column}") return True except Exception as e: logger.warning(f"⚠️ Model persistence load failed: {e}, trying legacy...") # FALLBACK: Legacy model.pkl loading save_dir = os.path.join(STORAGE_PATH, user_id) os.makedirs(save_dir, exist_ok=True) path = os.path.join(save_dir, "model.pkl") if not os.path.exists(path): return False try: with open(path, 'rb') as f: data = pickle.load(f) self.model = data['model'] self.model_name = data['model_name'] self.task_type = data.get('task_type', '') self.task_type_simple = data.get('task_type_simple', '') self.n_classes = data.get('n_classes', 0) self.target_column = data.get('target_column', '') self.feature_columns = data.get('feature_columns', []) self.numeric_cols = data.get('numeric_cols', []) self.categorical_cols = data.get('categorical_cols', []) self.text_cols = data.get('text_cols', []) self.label_encoders = data.get('label_encoders', {}) self.target_encoder = data.get('target_encoder') self.text_vectorizers = data.get('text_vectorizers', {}) self.scaler = data.get('scaler') self.numeric_fill_values = data.get('numeric_fill_values', {}) self.feature_metadata = data.get('feature_metadata', []) self.metrics = data.get('metrics', {}) self.confusion_matrix = data.get('confusion_matrix') self._y_test = data.get('y_test') self._y_pred = data.get('y_pred') # NEW: Restore NLP and Advanced Feature Engineering state self.is_nlp_task = data.get('is_nlp_task', False) self.primary_text_col = data.get('primary_text_col') self.nlp_scaler = data.get('nlp_scaler') if 'feature_engineer' in data: self.feature_engineer = data['feature_engineer'] # Restore Production Pipeline self.production_mode = data.get('production_mode', False) if 'production_engineer' in data: self.production_engineer = data['production_engineer'] print(f"📂 Loaded from {path} (legacy)") return True except Exception as e: print(f"⚠️ Load error: {e}") return False def get_model_metrics(self) -> Dict[str, Any]: """Get model performance metrics""" metrics = getattr(self, 'metrics', {}) result = { 'model_name': self.model_name, 'task_type': self.task_type, 'target': self.target_column, 'n_features': len(self.feature_columns), 'metrics': metrics, } # Add confusion matrix for classification if self.task_type_simple == 'classification': cm = getattr(self, 'confusion_matrix', None) if cm is not None: result['confusion_matrix'] = cm.tolist() if hasattr(cm, 'tolist') else cm # Calculate from stored predictions y_test = getattr(self, '_y_test', None) y_pred = getattr(self, '_y_pred', None) if y_test is not None and y_pred is not None: try: from sklearn.metrics import confusion_matrix, accuracy_score, f1_score, precision_score, recall_score cm = confusion_matrix(y_test, y_pred) result['confusion_matrix'] = cm.tolist() result['metrics']['accuracy'] = float(accuracy_score(y_test, y_pred)) result['metrics']['f1'] = float(f1_score(y_test, y_pred, average='weighted')) result['metrics']['precision'] = float(precision_score(y_test, y_pred, average='weighted', zero_division=0)) result['metrics']['recall'] = float(recall_score(y_test, y_pred, average='weighted', zero_division=0)) # Add ROC-AUC try: y_proba = getattr(self, '_y_proba', None) n_classes = len(np.unique(y_test)) if n_classes == 2 and y_proba is not None: result['metrics']['roc_auc'] = float(roc_auc_score(y_test, y_proba[:, 1])) elif n_classes > 2 and y_proba is not None: result['metrics']['roc_auc'] = float(roc_auc_score( y_test, y_proba, multi_class='ovr', average='weighted' )) except Exception: pass except Exception as e: print(f"Metrics calc error: {e}") return result def get_feature_metadata(self) -> List[Dict]: return self.feature_metadata # ========================================================================= # UNSUPERVISED LEARNING - CLUSTERING # ========================================================================= def run_clustering( self, df: pd.DataFrame, n_clusters: int = None, algorithm: str = 'kmeans' ) -> Dict[str, Any]: """ Run unsupervised clustering on the data. Args: df: DataFrame with numeric features n_clusters: Number of clusters (auto-detected if None) algorithm: 'kmeans', 'dbscan', or 'hierarchical' Returns: Dict with cluster labels, metrics, and visualization data """ logger.info(f"🔮 Running {algorithm} clustering...") result = { 'success': False, 'algorithm': algorithm, 'n_clusters': 0, 'labels': [], 'metrics': {}, 'pca_2d': None, 'charts': {} } try: # Prepare numeric data numeric_df = df.select_dtypes(include=[np.number]).dropna() if numeric_df.empty or len(numeric_df) < 10: result['error'] = "Not enough numeric data for clustering" return result # Scale the data scaler = StandardScaler() X_scaled = scaler.fit_transform(numeric_df) # Auto-detect optimal clusters if not specified if n_clusters is None and algorithm != 'dbscan': n_clusters = self._find_optimal_clusters(X_scaled) # Run clustering if algorithm == 'kmeans': model = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) labels = model.fit_predict(X_scaled) elif algorithm == 'dbscan': model = DBSCAN(eps=0.5, min_samples=5) labels = model.fit_predict(X_scaled) n_clusters = len(set(labels)) - (1 if -1 in labels else 0) elif algorithm == 'hierarchical': model = AgglomerativeClustering(n_clusters=n_clusters) labels = model.fit_predict(X_scaled) elif algorithm == 'gmm': model = GaussianMixture(n_components=n_clusters, random_state=42) labels = model.fit_predict(X_scaled) elif algorithm == 'spectral': # Use fewer nearest neighbors for spectral to avoid graph connectivity issues model = SpectralClustering(n_clusters=n_clusters, random_state=42, affinity='nearest_neighbors') labels = model.fit_predict(X_scaled) else: result['error'] = f"Unknown algorithm: {algorithm}" return result # Calculate metrics if len(set(labels)) > 1 and -1 not in labels: silhouette = silhouette_score(X_scaled, labels) else: silhouette = 0.0 # PCA for visualization pca = PCA(n_components=2) X_2d = pca.fit_transform(X_scaled) # Store results result['success'] = True result['n_clusters'] = n_clusters result['labels'] = labels.tolist() result['metrics'] = { 'silhouette_score': float(silhouette), 'n_samples': len(labels), 'pca_variance_explained': float(sum(pca.explained_variance_ratio_)) } result['pca_2d'] = X_2d.tolist() # Generate cluster scatter chart (base64 image) import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import io import base64 fig, ax = plt.subplots(figsize=(10, 8)) # Color palette colors = ['#2563eb', '#16a34a', '#dc2626', '#ea580c', '#9333ea', '#0891b2', '#db2777', '#d97706', '#0d9488', '#4f46e5'] unique_labels = sorted(set(labels)) for i, label in enumerate(unique_labels): mask = labels == label color = colors[i % len(colors)] if label >= 0 else '#6b7280' label_name = f'Cluster {label}' if label >= 0 else 'Noise' ax.scatter(X_2d[mask, 0], X_2d[mask, 1], c=color, label=label_name, alpha=0.7, s=50, edgecolors='white', linewidth=0.5) ax.set_xlabel('PCA Component 1', fontweight='bold', fontsize=12) ax.set_ylabel('PCA Component 2', fontweight='bold', fontsize=12) ax.set_title(f'🔮 {algorithm.title()} Clustering (Silhouette: {silhouette:.3f})', fontweight='bold', pad=15, fontsize=14) ax.legend(loc='best', fontsize=10) ax.grid(alpha=0.3) ax.set_facecolor('#ffffff') fig.set_facecolor('#f8f9fa') # Convert to base64 buf = io.BytesIO() fig.savefig(buf, format='png', dpi=150, bbox_inches='tight', facecolor='#f8f9fa', edgecolor='none') buf.seek(0) cluster_chart = f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode('utf-8')}" plt.close(fig) result['charts']['cluster_scatter'] = cluster_chart # Add cluster distribution unique, counts = np.unique(labels, return_counts=True) result['cluster_distribution'] = { f"Cluster {k}" if k >= 0 else "Noise": int(v) for k, v in zip(unique, counts) } logger.info(f"✅ Clustering complete: {n_clusters} clusters, silhouette={silhouette:.3f}") except Exception as e: logger.error(f"Clustering error: {e}") result['error'] = str(e) return result def _find_optimal_clusters(self, X: np.ndarray, max_k: int = 10) -> int: """Find optimal number of clusters using elbow method + silhouette""" logger.info("🔍 Finding optimal number of clusters...") # CRITICAL: Sample for silhouette calculation (O(n²) complexity!) # 10K samples is max to keep computation reasonable MAX_SILHOUETTE_SAMPLES = 10000 if len(X) > MAX_SILHOUETTE_SAMPLES: logger.info(f" ⚠️ Sampling {MAX_SILHOUETTE_SAMPLES} rows for silhouette (dataset has {len(X)})") sample_idx = np.random.choice(len(X), MAX_SILHOUETTE_SAMPLES, replace=False) X_sample = X[sample_idx] else: X_sample = X sample_idx = None max_k = min(max_k, len(X_sample) - 1) k_range = range(2, max_k + 1) inertias = [] silhouettes = [] for k in k_range: # Fit on full data for accurate inertia kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) full_labels = kmeans.fit_predict(X) inertias.append(kmeans.inertia_) # Calculate silhouette on SAMPLE only (to avoid O(n²) on full data) if sample_idx is not None: sample_labels = full_labels[sample_idx] silhouettes.append(silhouette_score(X_sample, sample_labels)) else: silhouettes.append(silhouette_score(X, full_labels)) # Best k by silhouette score best_k = list(k_range)[np.argmax(silhouettes)] logger.info(f" ✅ Optimal K = {best_k} (silhouette = {max(silhouettes):.3f})") return best_k def get_all_ml_charts(self) -> Dict[str, Any]: """Get all ML charts for the trained model using enhanced chart generator""" try: from ml.chart_generator import generate_ml_charts, generate_model_comparison_chart y_test = getattr(self, '_y_test', None) y_pred = getattr(self, '_y_pred', None) y_proba = getattr(self, '_y_proba', None) if y_test is None or y_pred is None: return {'error': 'No model trained yet'} # Get feature importance importance = self._get_importance(self.model) if self.model else [] # Get class names for classification class_names = None if self.target_encoder is not None: class_names = self.target_encoder.classes_.tolist() # Generate all charts using the new generator charts = generate_ml_charts( task_type=self.task_type, y_test=y_test, y_pred=y_pred, y_proba=y_proba, feature_importance=importance, class_names=class_names, model_name=getattr(self, 'model_name', 'Model') ) logger.info(f"✅ Generated {len(charts)} ML charts") return charts except Exception as e: logger.error(f"Chart generation error: {e}") return {'error': str(e)} def god_level_train( self, df: pd.DataFrame, target_col: str = None, user_id: str = "default", mode: str = "ultra", algorithm: str = None ) -> TrainResult: """ 🔱 GOD-LEVEL AUTOML TRAINING ============================ Uses the advanced GOD-Level AutoML engine for: - Complete data intelligence - Advanced leakage detection - Intelligent model selection - Safe training with overfitting protection - Model reliability scoring Returns a TrainResult for compatibility with existing UI. """ try: from ml.god_level_automl import god_level_train, GodLevelResult logger.info("🔱 Running GOD-Level AutoML Training...") # Run GOD-Level training result = god_level_train(df, target_col, user_id, mode, algorithm) if not result.success: raise ValueError(result.warnings[0] if result.warnings else "GOD-Level training failed") # Convert to TrainResult for compatibility train_result = TrainResult( success=True, task_type=result.problem_type, target_column=result.target_column, feature_columns=result.feature_columns, best_model_name=result.best_model_name, best_model_metrics=result.best_model_metrics, leaderboard=result.leaderboard, feature_importance=result.feature_importance, y_test=result.y_test, y_pred=result.y_pred, y_proba=result.y_proba, feature_metadata=result.feature_metadata, n_rows=result.n_rows, n_cols=result.n_cols, processing_time=result.processing_time, charts=result.charts, is_nlp_task=False, primary_text_col=None ) return train_result except ImportError: logger.warning("GOD-Level AutoML not available, falling back to production_train") return self.production_train(df, target_col, user_id, mode) except Exception as e: logger.error(f"GOD-Level training failed: {e}") import traceback traceback.print_exc() raise # Global instance automl_engine = ProductionMLEngine()