""" 🧠 SMART ALGORITHM SELECTOR v1.0 ================================ Intelligently selects algorithms based on dataset characteristics: - Data type (tabular, text, time-series) - Dataset size (small/medium/large) - Feature types (numeric, categorical, text) - Task type (classification, regression) - Imbalance level This ensures Ultra Mode trains ONLY relevant algorithms, not wasting time on inappropriate models. Author: AI Business Analyst Team Version: 1.0.0 """ from typing import List, Dict, Any, Optional from dataclasses import dataclass from enum import Enum import logging logger = logging.getLogger(__name__) # ============================================================================= # ALGORITHM DEFINITIONS # ============================================================================= class AlgorithmCategory(Enum): """Categories of ML algorithms""" TREE_BASED = "tree_based" LINEAR = "linear" SVM = "svm" NEURAL_NET = "neural_net" ENSEMBLE = "ensemble" NAIVE_BAYES = "naive_bayes" NEIGHBORS = "neighbors" DEEP_LEARNING = "deep_learning" @dataclass class AlgorithmInfo: """Information about an algorithm""" name: str category: AlgorithmCategory supports_class_weight: bool min_samples: int # Minimum samples needed good_for_text: bool good_for_timeseries: bool good_for_tabular: bool training_speed: str # 'fast', 'medium', 'slow' # Algorithm database ALGORITHMS = { # === TREE-BASED (Great for tabular) === 'RandomForest': AlgorithmInfo( 'RandomForest', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=50, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='medium' ), 'XGBoost': AlgorithmInfo( 'XGBoost', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=100, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='medium' ), 'LightGBM': AlgorithmInfo( 'LightGBM', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=100, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='fast' ), 'CatBoost': AlgorithmInfo( 'CatBoost', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=100, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='slow' ), 'GradientBoosting': AlgorithmInfo( 'GradientBoosting', AlgorithmCategory.TREE_BASED, supports_class_weight=False, min_samples=50, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='medium' ), 'ExtraTrees': AlgorithmInfo( 'ExtraTrees', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=50, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='medium' ), 'DecisionTree': AlgorithmInfo( 'DecisionTree', AlgorithmCategory.TREE_BASED, supports_class_weight=True, min_samples=20, good_for_text=False, good_for_timeseries=True, good_for_tabular=True, training_speed='fast' ), # === LINEAR MODELS === 'LogisticRegression': AlgorithmInfo( 'LogisticRegression', AlgorithmCategory.LINEAR, supports_class_weight=True, min_samples=20, good_for_text=True, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' ), 'RidgeClassifier': AlgorithmInfo( 'RidgeClassifier', AlgorithmCategory.LINEAR, supports_class_weight=True, min_samples=20, good_for_text=True, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' ), 'SGD': AlgorithmInfo( 'SGD', AlgorithmCategory.LINEAR, supports_class_weight=True, min_samples=100, good_for_text=True, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' ), # === SVM === 'SVM': AlgorithmInfo( 'SVM', AlgorithmCategory.SVM, supports_class_weight=True, min_samples=50, good_for_text=True, good_for_timeseries=False, good_for_tabular=True, training_speed='slow' # O(n^2) complexity ), # === NEURAL NETWORKS (sklearn) === 'MLP': AlgorithmInfo( 'MLP', AlgorithmCategory.NEURAL_NET, supports_class_weight=False, min_samples=500, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='medium' ), # === DEEP LEARNING (TensorFlow) === 'DeepANN': AlgorithmInfo( 'DeepANN', AlgorithmCategory.DEEP_LEARNING, supports_class_weight=True, min_samples=1000, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='slow' ), 'LSTM': AlgorithmInfo( 'LSTM', AlgorithmCategory.DEEP_LEARNING, supports_class_weight=True, min_samples=500, good_for_text=True, good_for_timeseries=True, good_for_tabular=False, training_speed='slow' ), 'TextCNN': AlgorithmInfo( 'TextCNN', AlgorithmCategory.DEEP_LEARNING, supports_class_weight=True, min_samples=1000, good_for_text=True, good_for_timeseries=False, good_for_tabular=False, training_speed='slow' ), # === NAIVE BAYES === 'GaussianNB': AlgorithmInfo( 'GaussianNB', AlgorithmCategory.NAIVE_BAYES, supports_class_weight=False, min_samples=20, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' ), 'BernoulliNB': AlgorithmInfo( 'BernoulliNB', AlgorithmCategory.NAIVE_BAYES, supports_class_weight=False, min_samples=20, good_for_text=True, good_for_timeseries=False, good_for_tabular=False, training_speed='fast' ), # === NEIGHBORS === 'KNN': AlgorithmInfo( 'KNN', AlgorithmCategory.NEIGHBORS, supports_class_weight=False, min_samples=50, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' # But slow at prediction for large datasets ), # === ENSEMBLE === 'AdaBoost': AlgorithmInfo( 'AdaBoost', AlgorithmCategory.ENSEMBLE, supports_class_weight=False, min_samples=100, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='medium' ), 'Bagging': AlgorithmInfo( 'Bagging', AlgorithmCategory.ENSEMBLE, supports_class_weight=False, min_samples=100, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='medium' ), 'HistGradientBoosting': AlgorithmInfo( 'HistGradientBoosting', AlgorithmCategory.ENSEMBLE, supports_class_weight=True, min_samples=1000, good_for_text=False, good_for_timeseries=False, good_for_tabular=True, training_speed='fast' # Histogram-based, very fast! ), } # ============================================================================= # SMART ALGORITHM SELECTOR # ============================================================================= class SmartAlgorithmSelector: """ 🧠 Intelligently selects algorithms based on dataset characteristics """ def __init__(self): self.algorithms = ALGORITHMS def select_algorithms( self, data_type: str, # 'tabular', 'text_heavy', 'time_series', 'mixed' task_type: str, # 'classification', 'regression' n_rows: int, n_features: int, is_imbalanced: bool = False, imbalance_ratio: float = 1.0, mode: str = 'fast' # 'fast' or 'ultra' ) -> Dict[str, List[str]]: """ Select optimal algorithms for the given dataset Returns: Dict with 'selected' (list of algorithm names) and 'skipped' (with reasons) """ logger.info(f"🧠 Smart Algorithm Selection for {data_type.upper()} data") logger.info(f" šŸ“Š Rows: {n_rows}, Features: {n_features}") logger.info(f" šŸŽŖ Task: {task_type}, Mode: {mode.upper()}") selected = [] skipped = {} # Determine which algorithms to consider if mode == 'fast': # Fast mode: Only essential, fast algorithms candidates = ['LogisticRegression', 'RandomForest', 'GradientBoosting', 'KNN', 'GaussianNB', 'XGBoost', 'LightGBM'] else: # Ultra mode: All algorithms candidates = list(self.algorithms.keys()) for name in candidates: algo = self.algorithms.get(name) if not algo: continue # Check minimum samples if n_rows < algo.min_samples: skipped[name] = f"Need {algo.min_samples}+ samples (have {n_rows})" continue # Check data type compatibility if data_type == 'tabular' and not algo.good_for_tabular: skipped[name] = "Not suitable for tabular data" continue elif data_type == 'text_heavy' and not algo.good_for_text: skipped[name] = "Not suitable for text data" continue elif data_type == 'time_series' and not algo.good_for_timeseries: skipped[name] = "Not suitable for time-series data" continue # For imbalanced data, prefer models with class_weight if is_imbalanced and imbalance_ratio > 20: if not algo.supports_class_weight: # Still include but note limitation logger.info(f" āš ļø {name}: No class_weight (will use sampling)") # SVM is too slow for large datasets if name == 'SVM' and n_rows > 10000: skipped[name] = f"Too slow for {n_rows} samples (O(n²))" continue # KNN is slow at prediction for large datasets if name == 'KNN' and n_rows > 50000: skipped[name] = f"Slow predictions for {n_rows} samples" continue # Deep learning needs TensorFlow if algo.category == AlgorithmCategory.DEEP_LEARNING: try: import tensorflow as tf selected.append(name) except ImportError: skipped[name] = "TensorFlow not installed" continue else: selected.append(name) # Log results logger.info(f" āœ… Selected: {len(selected)} algorithms") logger.info(f" ā­ļø Skipped: {len(skipped)} algorithms") for name in selected[:5]: # Show first 5 logger.info(f" āœ“ {name}") if len(selected) > 5: logger.info(f" ... and {len(selected) - 5} more") return { 'selected': selected, 'skipped': skipped } def get_algorithm_info(self, name: str) -> Optional[AlgorithmInfo]: """Get info about a specific algorithm""" return self.algorithms.get(name) # ============================================================================= # TENSORFLOW NEURAL NETWORK MODELS # ============================================================================= def create_deep_ann_classifier( input_dim: int, n_classes: int, hidden_layers: tuple = (256, 128, 64, 32), dropout_rate: float = 0.3 ): """ Create a Deep ANN classifier using TensorFlow/Keras Architecture: - Input → Dense(256) → BatchNorm → Dropout - Dense(128) → BatchNorm → Dropout - Dense(64) → BatchNorm → Dropout - Dense(32) → Output """ try: import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ # Input layer layers.InputLayer(input_shape=(input_dim,)), # Hidden layers with batch normalization and dropout layers.Dense(hidden_layers[0], activation='relu'), layers.BatchNormalization(), layers.Dropout(dropout_rate), layers.Dense(hidden_layers[1], activation='relu'), layers.BatchNormalization(), layers.Dropout(dropout_rate), layers.Dense(hidden_layers[2], activation='relu'), layers.BatchNormalization(), layers.Dropout(dropout_rate / 2), layers.Dense(hidden_layers[3], activation='relu'), # Output layer layers.Dense(n_classes, activation='softmax' if n_classes > 2 else 'sigmoid') ]) # Compile with appropriate loss if n_classes > 2: loss = 'sparse_categorical_crossentropy' else: loss = 'binary_crossentropy' model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.001), loss=loss, metrics=['accuracy'] ) return model except ImportError: logger.warning("TensorFlow not available, skipping DeepANN") return None def create_lstm_classifier( seq_length: int, n_features: int, n_classes: int, hidden_units: int = 64 ): """ Create an LSTM classifier for time-series/sequential data """ try: import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ layers.InputLayer(input_shape=(seq_length, n_features)), layers.LSTM(hidden_units, return_sequences=True), layers.Dropout(0.2), layers.LSTM(hidden_units // 2), layers.Dropout(0.2), layers.Dense(32, activation='relu'), layers.Dense(n_classes, activation='softmax' if n_classes > 2 else 'sigmoid') ]) if n_classes > 2: loss = 'sparse_categorical_crossentropy' else: loss = 'binary_crossentropy' model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.001), loss=loss, metrics=['accuracy'] ) return model except ImportError: logger.warning("TensorFlow not available, skipping LSTM") return None def create_text_cnn_classifier( vocab_size: int, embed_dim: int = 128, n_classes: int = 2, max_length: int = 500, num_filters: int = 128 ): """ Create a 1D CNN classifier for text classification Uses multiple filter sizes (3, 4, 5) to capture different n-gram patterns """ try: import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers # Input inputs = layers.Input(shape=(max_length,)) # Embedding x = layers.Embedding(vocab_size, embed_dim)(inputs) # Multiple Conv1D with different filter sizes conv_outputs = [] for filter_size in [3, 4, 5]: conv = layers.Conv1D(num_filters, filter_size, activation='relu')(x) pool = layers.GlobalMaxPooling1D()(conv) conv_outputs.append(pool) # Concatenate all conv outputs concat = layers.Concatenate()(conv_outputs) # Dense layers x = layers.Dropout(0.5)(concat) x = layers.Dense(64, activation='relu')(x) # Output if n_classes > 2: outputs = layers.Dense(n_classes, activation='softmax')(x) loss = 'sparse_categorical_crossentropy' else: outputs = layers.Dense(1, activation='sigmoid')(x) loss = 'binary_crossentropy' model = keras.Model(inputs, outputs) model.compile( optimizer=keras.optimizers.Adam(learning_rate=0.001), loss=loss, metrics=['accuracy'] ) return model except ImportError: logger.warning("TensorFlow not available, skipping TextCNN") return None # ============================================================================= # SKLEARN WRAPPER FOR KERAS MODELS # ============================================================================= class KerasClassifierWrapper: """ Wrapper to make Keras models sklearn-compatible """ def __init__(self, model, epochs=50, batch_size=32, class_weight=None): self.model = model self.epochs = epochs self.batch_size = batch_size self.class_weight = class_weight self._is_fitted = False def fit(self, X, y): """Train the model""" try: import tensorflow as tf # Early stopping early_stop = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ) # Calculate class weights for imbalanced data if self.class_weight == 'balanced': from sklearn.utils.class_weight import compute_class_weight import numpy as np classes = np.unique(y) weights = compute_class_weight('balanced', classes=classes, y=y) self.class_weight = dict(zip(classes, weights)) # Train with validation split self.model.fit( X, y, epochs=self.epochs, batch_size=self.batch_size, validation_split=0.2, callbacks=[early_stop], class_weight=self.class_weight, verbose=0 ) self._is_fitted = True return self except Exception as e: logger.error(f"Keras training error: {e}") raise def predict(self, X): """Make predictions""" import numpy as np proba = self.model.predict(X, verbose=0) if proba.shape[1] == 1: return (proba > 0.5).astype(int).flatten() return np.argmax(proba, axis=1) def predict_proba(self, X): """Get prediction probabilities""" import numpy as np proba = self.model.predict(X, verbose=0) if proba.shape[1] == 1: return np.hstack([1 - proba, proba]) return proba @property def classes_(self): """Return class labels""" import numpy as np n_classes = self.model.output_shape[-1] if n_classes == 1: return np.array([0, 1]) return np.arange(n_classes) # Global instance smart_selector = SmartAlgorithmSelector()