{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":114135,"databundleVersionId":13695648,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#!/usr/bin/env python3\n\n# =================== Kaggle CPU Baseline (TỐI ƯU HÓA) =====================\n# - Giữ lại ý tưởng pipeline và grid search cốt lõi.\n# - NÂNG CẤP: Thêm LightGBM (LGBMClassifier), một mô hình boosting mạnh mẽ,\n#   vào quá trình đánh giá để tìm ra mô hình tốt nhất.\n# - NÂNG CẤP: Thêm bước Feature Engineering dựa trên thời gian và ticker,\n#   tạo ra các đặc trưng rolling window và date-based để nắm bắt xu hướng.\n# - TỐI ƯU: Tinh chỉnh không gian tìm kiếm và thêm logging chi tiết hơn.\n# - GIỮ NGUYÊN: Vẫn sử dụng StratifiedGroupKFold để tránh data leakage giữa các ticker.\n# ============================================================================\n\n!pip install pandas numpy scikit-learn imbalanced-learn lightgbm -q\n\nimport pandas as pd\nimport numpy as np\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Any, Optional, Tuple\nimport hashlib, json, itertools, warnings\nfrom tqdm.auto import tqdm\n\n# Tắt các cảnh báo không cần thiết\nwarnings.filterwarnings('ignore')\n\n# --- Imports từ Scikit-learn, Imbalanced-learn và LightGBM ---\nfrom sklearn.preprocessing import (\n    StandardScaler, RobustScaler, MinMaxScaler, QuantileTransformer,\n    Normalizer, MaxAbsScaler, KBinsDiscretizer\n)\nfrom sklearn.decomposition import PCA, TruncatedSVD, FastICA, FactorAnalysis\nfrom sklearn.feature_selection import (\n    VarianceThreshold, SelectKBest, SelectPercentile,\n    f_classif, mutual_info_classif, chi2\n)\nfrom sklearn.linear_model import LogisticRegression, RidgeClassifier\nfrom sklearn.model_selection import StratifiedGroupKFold, cross_val_score\nfrom sklearn.metrics import accuracy_score\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom imblearn.over_sampling import RandomOverSampler\nimport lightgbm as lgb\n\n# ---------------------- DataType utils ----------------------\nclass DataTypeHandler:\n    @staticmethod\n    def identify_feature_types(X, threshold=3):\n        n_samples, n_features = X.shape\n        boolean_mask = np.zeros(n_features, dtype=bool)\n        for i in range(n_features):\n            col = X[:, i]\n            unique_vals = np.unique(col[~np.isnan(col)])\n            if len(unique_vals) <= 2:\n                boolean_mask[i] = True\n        return boolean_mask\n\n    @staticmethod\n    def safe_variance_threshold(X, threshold=0.01):\n        keep = []\n        for i in range(X.shape[1]):\n            col = X[:, i].astype(float)\n            if np.var(col) > threshold:\n                keep.append(i)\n        return np.array(keep)\n\n# ---------------------- Pipeline dataclasses (Không thay đổi nhiều) ----------------------\n@dataclass\nclass ProcessingInput:\n    X: np.ndarray\n    y: np.ndarray\n    boolean_mask: Optional[np.ndarray] = None\n    metadata: Dict[str, Any] = field(default_factory=dict)\n\n@dataclass\nclass ProcessingOutput:\n    X: np.ndarray\n    y: np.ndarray\n    metadata: Dict[str, Any] = field(default_factory=dict)\n    transformation_log: List[str] = field(default_factory=list)\n\n@dataclass\nclass StepConfig:\n    enabled: bool = True\n    method: str = \"default\"\n    params: Dict[str, Any] = field(default_factory=dict)\n\n@dataclass\nclass PipelineConfig:\n    config_id: str = \"\"\n    outlier_removal: StepConfig = field(default_factory=lambda: StepConfig(enabled=False))\n    scaling: StepConfig = field(default_factory=lambda: StepConfig(enabled=True, method=\"standard\"))\n    feature_selection: StepConfig = field(default_factory=lambda: StepConfig(enabled=False))\n    dimensionality_reduction: StepConfig = field(default_factory=lambda: StepConfig(enabled=False))\n    balancing: StepConfig = field(default_factory=lambda: StepConfig(enabled=False))\n    def to_dict(self):\n        return {\n            'outlier_removal': {'enabled': self.outlier_removal.enabled, 'method': self.outlier_removal.method},\n            'scaling': {'enabled': self.scaling.enabled, 'method': self.scaling.method},\n            'feature_selection': {'enabled': self.feature_selection.enabled, 'method': self.feature_selection.method},\n            'dimensionality_reduction': {'enabled': self.dimensionality_reduction.enabled, 'method': self.dimensionality_reduction.method},\n            'balancing': {'enabled': self.balancing.enabled, 'method': self.balancing.method}\n        }\n    def generate_id(self):\n        self.config_id = hashlib.md5(json.dumps(self.to_dict(), sort_keys=True).encode()).hexdigest()[:8]\n        return self.config_id\n\n# ---------------------- Steps (Không thay đổi) ----------------------\n# Giữ nguyên các class Step vì chúng đã được thiết kế module hóa tốt\nclass ExpandedOutlierRemovalStep:\n    def __init__(self, config: StepConfig): self.config = config; self.bounds = {}; self.boolean_mask = None\n    def fit(self, data: ProcessingInput):\n        if not self.config.enabled: return self\n        self.boolean_mask = data.boolean_mask if data.boolean_mask is not None else np.zeros(data.X.shape[1], dtype=bool)\n        cont_idx = np.where(~self.boolean_mask)[0]\n        if len(cont_idx) == 0: return self\n        method = self.config.method\n        if method == \"iqr\":\n            mult = self.config.params.get('multiplier', 1.5)\n            for i in cont_idx:\n                Q1, Q3 = np.percentile(data.X[:, i], 25), np.percentile(data.X[:, i], 75)\n                IQR = Q3 - Q1; self.bounds[i] = (Q1 - mult*IQR, Q3 + mult*IQR)\n        elif method == \"percentile\":\n            lo, hi = self.config.params.get('lower', 1), self.config.params.get('upper', 99)\n            for i in cont_idx: self.bounds[i] = (np.percentile(data.X[:, i], lo), np.percentile(data.X[:, i], hi))\n        return self\n    def transform(self, data: ProcessingInput):\n        if not self.config.enabled: return ProcessingOutput(X=data.X, y=data.y, metadata=data.metadata, transformation_log=[\"outlier: skipped\"])\n        X = data.X.copy().astype(float)\n        for i,(lo,hi) in self.bounds.items(): X[:, i] = np.clip(X[:, i], lo, hi)\n        return ProcessingOutput(X=X, y=data.y, metadata=data.metadata, transformation_log=[f\"outlier:{self.config.method}\"])\n\nclass ExpandedScalingStep:\n    def __init__(self, config: StepConfig): self.config = config; self.scaler = None; self.boolean_mask = None; self.cont_idx = None\n    def fit(self, data: ProcessingInput):\n        if not self.config.enabled: return self\n        self.boolean_mask = data.boolean_mask if data.boolean_mask is not None else np.zeros(data.X.shape[1], dtype=bool)\n        self.cont_idx = np.where(~self.boolean_mask)[0]\n        if len(self.cont_idx)==0: return self\n        Xc = data.X[:, self.cont_idx].astype(float)\n        m = self.config.method\n        if m == \"standard\": self.scaler = StandardScaler()\n        elif m == \"robust\": self.scaler = RobustScaler()\n        elif m == \"minmax\": self.scaler = MinMaxScaler()\n        elif m == \"quantile_uniform\": self.scaler = QuantileTransformer(output_distribution='uniform', n_quantiles=min(1000, len(Xc)), random_state=42)\n        else: self.scaler = StandardScaler()\n        try: self.scaler.fit(Xc)\n        except: self.scaler = StandardScaler().fit(Xc)\n        return self\n    def transform(self, data: ProcessingInput):\n        if not self.config.enabled or self.scaler is None: return ProcessingOutput(X=data.X, y=data.y, metadata=data.metadata, transformation_log=[\"scaling: skipped\"])\n        X = data.X.copy().astype(float)\n        if len(self.cont_idx)>0:\n            try: X[:, self.cont_idx] = self.scaler.transform(X[:, self.cont_idx])\n            except: pass\n        return ProcessingOutput(X=X, y=data.y, metadata=data.metadata, transformation_log=[f\"scaling:{self.config.method}\"])\n\nclass ExpandedFeatureSelectionStep:\n    def __init__(self, config: StepConfig): self.config = config; self.selected = None\n    def fit(self, data: ProcessingInput):\n        if not self.config.enabled: return self\n        Xf, y = data.X.astype(float), data.y\n        m = self.config.method\n        k = min(self.config.params.get('k', 500), Xf.shape[1])\n        if m == \"kbest_f\": self.selected = np.where(SelectKBest(f_classif, k=k).fit(Xf, y).get_support())[0]\n        elif m == \"kbest_mutual\": self.selected = np.where(SelectKBest(mutual_info_classif, k=k).fit(Xf, y).get_support())[0]\n        if self.selected is None or len(self.selected) < 10: self.selected = np.arange(min(500, Xf.shape[1]))\n        return self\n    def transform(self, data: ProcessingInput):\n        if not self.config.enabled or self.selected is None: return ProcessingOutput(X=data.X, y=data.y, metadata=data.metadata, transformation_log=[\"feature: skipped\"])\n        return ProcessingOutput(X=data.X[:, self.selected], y=data.y, metadata=data.metadata, transformation_log=[f\"feature:{self.config.method}({len(self.selected)})\"])\n\nclass ExpandedDimensionalityReductionStep:\n    def __init__(self, config: StepConfig): self.config = config; self.reducer = None\n    def fit(self, data: ProcessingInput):\n        if not self.config.enabled: return self\n        n_components = min(self.config.params.get('n_components', 100), min(data.X.shape)-1)\n        if n_components < 2: return self\n        m = self.config.method\n        try:\n            if m == \"pca\": self.reducer = PCA(n_components=n_components, random_state=42).fit(data.X)\n            elif m == \"svd\": self.reducer = TruncatedSVD(n_components=n_components, random_state=42).fit(data.X)\n        except: self.reducer = None\n        return self\n    def transform(self, data: ProcessingInput):\n        if not self.config.enabled or self.reducer is None: return ProcessingOutput(X=data.X, y=data.y, metadata=data.metadata, transformation_log=[\"dim: skipped\"])\n        try: X = self.reducer.transform(data.X.astype(float))\n        except: X = data.X\n        return ProcessingOutput(X=X, y=data.y, metadata=data.metadata, transformation_log=[f\"dim:{self.config.method}\"])\n\nclass ExpandedBalancingStep:\n    def __init__(self, config: StepConfig): self.config = config; self.balancer = None\n    def fit(self, data: ProcessingInput):\n        if not self.config.enabled: return self\n        self.balancer = RandomOverSampler(random_state=42)\n        return self\n    def transform(self, data: ProcessingInput):\n        if not self.config.enabled or self.balancer is None: return ProcessingOutput(X=data.X, y=data.y, metadata=data.metadata, transformation_log=[\"balance: skipped\"])\n        Xb, yb = self.balancer.fit_resample(data.X.astype(float), data.y)\n        return ProcessingOutput(X=Xb, yb=yb, metadata=data.metadata, transformation_log=[\"balance: oversample\"])\n\n# ---------------------- Comprehensive Pipeline (Không thay đổi) ----------------------\nclass ComprehensivePipeline:\n    def __init__(self, config: PipelineConfig, boolean_mask=None):\n        self.config = config\n        self.boolean_mask = boolean_mask.copy() if boolean_mask is not None else None\n        self.steps = [\n            ExpandedOutlierRemovalStep(self.config.outlier_removal),\n            ExpandedScalingStep(self.config.scaling),\n            ExpandedFeatureSelectionStep(self.config.feature_selection),\n            ExpandedDimensionalityReductionStep(self.config.dimensionality_reduction),\n            ExpandedBalancingStep(self.config.balancing)\n        ]\n    def fit(self, X: np.ndarray, y: np.ndarray):\n        data = ProcessingInput(X=X, y=y, boolean_mask=self.boolean_mask)\n        temp_bool_mask = self.boolean_mask\n        for step in self.steps:\n            step.fit(data)\n            out = step.transform(data)\n            if isinstance(step, ExpandedFeatureSelectionStep) and temp_bool_mask is not None and hasattr(step, \"selected\"):\n                temp_bool_mask = temp_bool_mask[step.selected] if step.selected is not None else temp_bool_mask\n            data = ProcessingInput(X=out.X, y=out.y, boolean_mask=temp_bool_mask)\n        return self\n    def transform(self, X: np.ndarray, y: Optional[np.ndarray] = None):\n        data = ProcessingInput(X=X, y=y if y is not None else np.zeros(X.shape[0]), boolean_mask=self.boolean_mask)\n        temp_bool_mask = self.boolean_mask\n        for i, step in enumerate(self.steps):\n            if i == len(self.steps) - 1 and (y is None or not self.config.balancing.enabled):\n                break\n            out = step.transform(data)\n            if isinstance(step, ExpandedFeatureSelectionStep) and temp_bool_mask is not None and hasattr(step, \"selected\"):\n                 temp_bool_mask = temp_bool_mask[step.selected] if step.selected is not None else temp_bool_mask\n            data = ProcessingInput(X=out.X, y=out.y, boolean_mask=temp_bool_mask)\n        return data.X, data.y\n    def fit_transform(self, X: np.ndarray, y: np.ndarray):\n        self.fit(X, y)\n        return self.transform(X, y)\n\n# ---------------------- NÂNG CẤP: Feature Engineering ----------------------\ndef create_time_series_features(df: pd.DataFrame, feature_cols: List[str]) -> pd.DataFrame:\n    \"\"\"Tạo các đặc trưng mới từ cột thời gian và theo từng ticker.\"\"\"\n    print(\"Bắt đầu tạo đặc trưng chuỗi thời gian...\")\n    df_out = df.copy()\n    \n    # Chuyển cột 't' sang dạng datetime\n    df_out['t'] = pd.to_datetime(df_out['t'])\n    \n    # 1. Đặc trưng dựa trên ngày tháng\n    df_out['time_month'] = df_out['t'].dt.month\n    df_out['time_week'] = df_out['t'].dt.isocalendar().week.astype(int)\n    df_out['time_dayofweek'] = df_out['t'].dt.dayofweek\n    \n    # 2. Đặc trưng rolling window (chỉ chọn 10 cột đầu tiên để làm ví dụ)\n    # Trong thực tế, bạn có thể cần phân tích để chọn các cột quan trọng hơn\n    rolling_cols = feature_cols[:10] \n    windows = [3, 7, 14] # Các cửa sổ thời gian\n    \n    # Sắp xếp để đảm bảo rolling hoạt động đúng\n    df_out = df_out.sort_values(by=['ticker_id', 't'])\n    \n    for ticker_id, group in tqdm(df_out.groupby('ticker_id'), desc=\"Tạo Rolling Features\"):\n        for col in rolling_cols:\n            for w in windows:\n                # Rolling mean\n                df_out.loc[group.index, f'{col}_roll_mean_{w}'] = group[col].rolling(window=w, min_periods=1).mean()\n                # Rolling std\n                df_out.loc[group.index, f'{col}_roll_std_{w}'] = group[col].rolling(window=w, min_periods=1).std()\n\n    # Điền các giá trị NaN có thể phát sinh từ rolling\n    df_out.fillna(method='bfill', inplace=True)\n    \n    print(f\"Hoàn thành! Đã thêm {len(df_out.columns) - len(df.columns)} đặc trưng mới.\")\n    return df_out\n\n# ---------------------- NÂNG CẤP: Orchestrator ----------------------\nclass GridSearchOrchestrator:\n    def __init__(self, X_train, y_train, boolean_mask, groups):\n        self.X_train = X_train\n        self.y_train = y_train\n        self.boolean_mask = boolean_mask\n        self.groups = groups\n        self.results = []\n        self.best_config = None\n        self.best_score = -np.inf\n        self.best_model = None\n        self.best_pipeline = None\n\n    def generate_all_configurations(self):\n        # Tinh chỉnh lại các lựa chọn để tập trung vào những phương pháp hiệu quả\n        outlier_options = [StepConfig(enabled=False), StepConfig(enabled=True, method=\"iqr\")]\n        scaling_options = [\n            StepConfig(enabled=False),\n            StepConfig(enabled=True, method=\"standard\"),\n            StepConfig(enabled=True, method=\"robust\"),\n            StepConfig(enabled=True, method=\"minmax\"),\n        ]\n        feature_selection_options = [\n            StepConfig(enabled=False),\n            StepConfig(enabled=True, method=\"kbest_f\", params={'k': 500}),\n        ]\n        dim_reduction_options = [\n            StepConfig(enabled=False),\n            StepConfig(enabled=True, method=\"svd\", params={'n_components': 128}),\n        ]\n        balancing_options = [StepConfig(enabled=False), StepConfig(enabled=True)]\n\n        combos = list(itertools.product(\n            outlier_options, scaling_options, feature_selection_options,\n            dim_reduction_options, balancing_options\n        ))\n        return [PipelineConfig(outlier_removal=c[0], scaling=c[1], feature_selection=c[2],\n                               dimensionality_reduction=c[3], balancing=c[4]).generate_id() and c for c in\n                [PipelineConfig(outlier_removal=c[0], scaling=c[1], feature_selection=c[2],\n                                dimensionality_reduction=c[3], balancing=c[4]) for c in combos]]\n\n    def evaluate_configuration(self, config, n_splits=5):\n        try:\n            pipe = ComprehensivePipeline(config, self.boolean_mask)\n            Xp, yp = pipe.fit_transform(self.X_train, self.y_train)\n\n            classes = np.unique(yp)\n            cw = compute_class_weight('balanced', classes=classes, y=yp)\n            cw_dict = dict(zip(classes, cw))\n\n            # NÂNG CẤP: Thêm LGBM vào danh sách mô hình cần đánh giá\n            models = {\n                \"logreg\": LogisticRegression(max_iter=1000, class_weight=cw_dict, random_state=42),\n                \"ridge\": RidgeClassifier(class_weight='balanced', random_state=42),\n                \"lgbm\": lgb.LGBMClassifier(random_state=42, n_jobs=-1, class_weight='balanced')\n            }\n\n            cv = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=42)\n            \n            best_local_score = -np.inf\n            best_local_model_name = None\n            \n            for name, mdl in models.items():\n                # Sử dụng cross_val_score để code gọn hơn\n                scores = cross_val_score(mdl, Xp, yp, cv=cv, groups=self.groups, scoring='accuracy', n_jobs=-1)\n                mean_acc = np.mean(scores)\n                \n                if mean_acc > best_local_score:\n                    best_local_score = mean_acc\n                    best_local_model_name = name\n\n            best_model_instance = models[best_local_model_name]\n            best_model_instance.fit(Xp, yp)\n            \n            return {\n                'config': config, 'cv_acc': best_local_score,\n                'model': best_model_instance, 'model_name': best_local_model_name,\n                'pipeline': pipe, 'shape': Xp.shape\n            }\n        except Exception as e:\n            print(f\"Lỗi khi đánh giá config: {e}\")\n            return None\n\n    def run_grid_search(self, max_configs=18, n_splits=5):\n        all_cfgs = self.generate_all_configurations()\n        total = len(all_cfgs)\n        print(f\"\\nTổng không gian cấu hình: {total} | Thử nghiệm {min(max_configs, total)} cấu hình.\")\n        \n        idx = np.linspace(0, total - 1, num=min(max_configs, total), dtype=int)\n        cfgs_to_test = [all_cfgs[i] for i in idx]\n\n        for i, cfg in enumerate(tqdm(cfgs_to_test, desc=\"Grid Search Progress\"), 1):\n            res = self.evaluate_configuration(cfg, n_splits)\n            if res is None:\n                continue\n            \n            print(f\"Config {i}/{len(cfgs_to_test)} (ID={cfg.config_id}) | \"\n                  f\"Model: {res['model_name']} | CV ACC={res['cv_acc']:.4f} | Shape={res['shape']}\")\n\n            if res['cv_acc'] > self.best_score:\n                self.best_score = res['cv_acc']\n                self.best_config = res['config']\n                self.best_model = res['model']\n                self.best_pipeline = res['pipeline']\n            self.results.append(res)\n            \n        self._print_summary()\n        return self.best_config, self.best_model, self.best_pipeline\n\n    def _print_summary(self):\n        print(\"\\n\" + \"=\"*80)\n        print(\"TÓM TẮT KẾT QUẢ GRID SEARCH (metric: Accuracy)\")\n        print(\"=\"*80)\n        if not self.results:\n            print(\"Không có cấu hình nào hợp lệ.\")\n            return\n\n        top_5 = sorted(self.results, key=lambda r: r['cv_acc'], reverse=True)[:5]\n        print(f\"Đã thử: {len(self.results)} cấu hình | CV ACC Tốt nhất: {self.best_score:.4f} | ID tốt nhất: {self.best_config.config_id}\")\n        \n        print(\"\\nTop 5 Cấu hình tốt nhất:\")\n        for i, r in enumerate(top_5, 1):\n            cfg = r['config']\n            parts = [\n                f\"outlier:{cfg.outlier_removal.method}\" if cfg.outlier_removal.enabled else \"no_outlier\",\n                f\"scale:{cfg.scaling.method}\" if cfg.scaling.enabled else \"no_scale\",\n                f\"feat_sel:{cfg.feature_selection.method}\" if cfg.feature_selection.enabled else \"no_feat_sel\",\n                f\"dim_red:{cfg.dimensionality_reduction.method}\" if cfg.dimensionality_reduction.enabled else \"no_dim_red\",\n                f\"balance:on\" if cfg.balancing.enabled else \"no_balance\",\n            ]\n            print(f\"  {i}. ACC={r['cv_acc']:.4f} | Model: {r['model_name']:<7} | Cấu hình: {' | '.join(parts)}\")\n\n# ---------------------- Main ----------------------\ndef main():\n    # --- Các hằng số có thể cấu hình ---\n    DATA_ROOT = \".\" # Thay đổi thành \"/kaggle/input/...\" nếu chạy trên Kaggle\n    BEST_CONFIGS_TO_TEST = 20 # Số lượng cấu hình để thử nghiệm\n    CV_SPLITS = 5 # Số fold cho cross-validation\n\n    print(\"1. Đang tải dữ liệu...\")\n    try:\n        train_df = pd.read_csv(f\"{DATA_ROOT}/train.csv\")\n        test_df = pd.read_csv(f\"{DATA_ROOT}/test.csv\")\n    except FileNotFoundError:\n        print(\"Lỗi: Không tìm thấy file train.csv/test.csv. Hãy đảm bảo chúng ở trong thư mục DATA_ROOT.\")\n        return\n\n    if set(train_df[\"class_label\"].dropna().unique()) - {\"H\",\"L\",\"None\"}:\n        mapping = {\"HH\":\"H\",\"LH\":\"H\",\"HL\":\"L\",\"LL\":\"L\"}\n        train_df[\"class_label\"] = train_df[\"class_label\"].map(mapping).fillna(\"None\")\n\n    meta_cols = [c for c in ['ticker_id', 't', 'class_label', 'id'] if c in train_df.columns or c in test_df.columns]\n    feature_cols = [c for c in train_df.columns if c not in meta_cols]\n\n    # NÂNG CẤP: Áp dụng Feature Engineering\n    train_df = create_time_series_features(train_df, feature_cols)\n    test_df = create_time_series_features(test_df, feature_cols)\n    \n    # Cập nhật lại danh sách các cột đặc trưng\n    feature_cols = [c for c in train_df.columns if c not in meta_cols]\n    \n    print(\"\\n2. Chuẩn bị dữ liệu cho mô hình...\")\n    X_train = train_df[feature_cols].values\n    y_train_str = train_df[\"class_label\"].values\n    classes = ['H','L','None']\n    class_map = {label: i for i, label in enumerate(classes)}\n    y_train = np.array([class_map[c] for c in y_train_str])\n    X_test = test_df[feature_cols].values\n\n    # Xử lý NaN, inf\n    X_train = np.nan_to_num(X_train, nan=0.0, posinf=np.finfo(np.float32).max, neginf=np.finfo(np.float32).min)\n    X_test = np.nan_to_num(X_test, nan=0.0, posinf=np.finfo(np.float32).max, neginf=np.finfo(np.float32).min)\n\n    print(\"Đang xác định loại đặc trưng (boolean/continuous)...\")\n    boolean_mask = DataTypeHandler.identify_feature_types(X_train)\n    print(f\"Tìm thấy: {boolean_mask.sum()} boolean, {X_train.shape[1] - boolean_mask.sum()} continuous.\")\n\n    groups = train_df[\"ticker_id\"].astype(str).values\n\n    print(\"\\n3. Bắt đầu Grid Search để tìm pipeline và mô hình tốt nhất...\")\n    orchestrator = GridSearchOrchestrator(X_train, y_train, boolean_mask, groups)\n    best_config, best_model, best_pipeline = orchestrator.run_grid_search(max_configs=BEST_CONFIGS_TO_TEST, n_splits=CV_SPLITS)\n\n    if best_model is not None:\n        print(\"\\n4. Huấn luyện mô hình tốt nhất trên toàn bộ dữ liệu training...\")\n        # Không cần fit_transform lại, vì pipeline đã được fit trong quá trình tìm kiếm\n        # Chỉ cần transform dữ liệu train và test\n        X_train_final, y_train_final = best_pipeline.transform(X_train, y_train)\n        X_test_final, _ = best_pipeline.transform(X_test)\n        \n        print(f\"Huấn luyện mô hình {type(best_model).__name__}...\")\n        best_model.fit(X_train_final, y_train_final)\n\n        print(\"5. Dự đoán trên tập test và tạo file submission...\")\n        pred_id = best_model.predict(X_test_final)\n        pred_str = [classes[i] for i in pred_id]\n\n        sub = pd.DataFrame({\n            \"id\": test_df[\"id\"],\n            \"class_label\": pred_str\n        })\n        sub.to_csv(\"submission.csv\", index=False)\n        print(\"\\nĐã lưu file submission.csv thành công!\")\n        print(\"Phân phối dự đoán:\")\n        print(sub[\"class_label\"].value_counts(normalize=True).mul(100).round(2).astype(str) + '%')\n\nif __name__ == \"__main__\":\n    main()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}