{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":103436,"databundleVersionId":12476472,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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 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# --- Thêm đoạn mã để kiểm tra sample_Solution.csv ---\nprint(\"\\n--- Kiểm tra tệp sample_Solution.csv ---\")\ntry:\n    sample_solution_path = \"/kaggle/input/ds-108-p-21-assigment-06/sample_Solution.csv\"\n    df_sample_solution = pd.read_csv(sample_solution_path)\n    print(f\"Đã tải thành công: {sample_solution_path}\")\n    df_sample_solution.info()\n    print(df_sample_solution.head())\nexcept FileNotFoundError:\n    print(f\"⚠️ Không tìm thấy tệp sample_Solution.csv tại đường dẫn: {sample_solution_path}. Vui lòng đảm bảo tệp tồn tại.\")\nexcept Exception as e:\n    print(f\"⚠️ Lỗi khi đọc tệp sample_Solution.csv: {e}\")\nprint(\"--- Kết thúc kiểm tra sample_Solution.csv ---\\n\")\n# ----------------------------------------------------\n\n# THÊM CÁC LỆNH CÀI ĐẶT/NÂNG CẤP THƯ VIỆN NÀY ĐỂ ĐẢM BẢO TƯƠNG THÍCH\n# Fix lỗi ModuleNotFoundError: No module named 'sklearn.utils._metadata_requests'\n# do xung đột phiên bản giữa scikit-learn và imbalanced-learn.\n# Môi trường Kaggle hiện tại có scikit-learn==1.2.2\n# imbalanced-learn 0.13.0 yêu cầu scikit-learn >= 1.3.2,\n# vì vậy chúng ta sẽ cài đặt imbalanced-learn 0.12.2 để tương thích.\nprint(\"Đang gỡ cài đặt phiên bản imbalanced-learn hiện tại (nếu có)...\")\n!pip uninstall -y imbalanced-learn\nprint(\"Đang cài đặt imbalanced-learn phiên bản 0.12.2 (tương thích với scikit-learn 1.2.2)...\")\n!pip install imbalanced-learn==0.12.2\n\n\n# Các thư viện khác cần thiết cho mô hình\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom sklearn.model_selection import KFold, RandomizedSearchCV\nfrom sklearn.metrics import f1_score, precision_recall_curve\nimport lightgbm as lgb\nfrom imblearn.over_sampling import SMOTE # Bây giờ sẽ import từ imbalanced-learn 0.12.2\n\n# --- 1. Hàm tải và kết hợp dữ liệu (Đã điều chỉnh cho môi trường Kaggle) ---\ndef load_and_combine_data_kaggle(base_path):\n    \"\"\"\n    Tải và kết hợp dữ liệu từ đường dẫn Kaggle.\n    \"\"\"\n    print(f\"Đang tải dữ liệu từ thư mục Kaggle: {base_path}\")\n\n    # Đường dẫn đầy đủ đến các file dữ liệu trong môi trường Kaggle\n    delay_46_path = os.path.join(base_path, \"delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv\")\n    delay_79_path = os.path.join(base_path, \"delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv\")\n    not_delay_46_path = os.path.join(base_path, \"not_delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv\")\n    not_delay_79_path = os.path.join(base_path, \"not_delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv\")\n    test_df_path = os.path.join(base_path, \"PILOT_10.csv\")\n\n    # Kiểm tra xem các file có tồn tại không\n    required_files = [delay_46_path, delay_79_path, not_delay_46_path, not_delay_79_path, test_df_path]\n    for f_path in required_files:\n        if not os.path.exists(f_path):\n            raise FileNotFoundError(f\"File không tìm thấy: {f_path}. Vui lòng kiểm tra lại đường dẫn và đảm bảo tất cả các file dữ liệu đã được tải về.\")\n\n    # Đọc các file dữ liệu\n    delay_46 = pd.read_csv(delay_46_path)\n    delay_79 = pd.read_csv(delay_79_path)\n    not_delay_46 = pd.read_csv(not_delay_46_path)\n    not_delay_79 = pd.read_csv(not_delay_79_path)\n    test_df = pd.read_csv(test_df_path)\n\n    # Tạo tập huấn luyện gộp\n    train_df = pd.concat([\n        delay_46,\n        delay_79,\n        not_delay_46,\n        not_delay_79\n    ], ignore_index=True)\n\n    print(f\"✅ Đã tải xong dữ liệu. Kích thước tập huấn luyện: {train_df.shape}, Kích thước tập kiểm tra: {test_df.shape}\")\n    return train_df, test_df\n\n# --- 2. Hàm tiền xử lý dữ liệu ---\ndef preprocess_data(train_df, test_df):\n    \"\"\"\n    Thực hiện tiền xử lý dữ liệu: xử lý thiếu, ép kiểu, tạo đặc trưng thời gian, mã hóa, chuẩn hóa.\n    Đảm bảo tính nhất quán của các cột giữa train và test.\n    \"\"\"\n    print(\"\\n--- Bắt đầu tiền xử lý dữ liệu ---\")\n\n    # Xử lý cột 'Order date': Ép kiểu và tạo đặc trưng thời gian\n    for df in [train_df, test_df]:\n        df['Order date'] = pd.to_datetime(df['Order date'], format='%Y-%m-%d %H:%M:%S', errors='coerce')\n        df['Order date'] = df['Order date'].fillna(pd.Timestamp('2022-01-01'))\n        df['order_year'] = df['Order date'].dt.year\n        df['order_month'] = df['Order date'].dt.month\n        df['order_day'] = df['Order date'].dt.day\n        df['order_dayofweek'] = df['Order date'].dt.dayofweek\n        df['order_is_weekend'] = df['order_dayofweek'].isin([5, 6]).astype(int)\n\n        # Xử lý SO_TIME\n        df['SO_HOUR'] = df['SO_TIME'] // 10000\n        df['SO_MINUTE'] = (df['SO_TIME'] // 100) % 100\n        df['SO_SECOND'] = df['SO_TIME'] % 100\n\n        df.drop(columns=['Order date', 'SO_TIME'], inplace=True)\n\n    print(\"✅ Đã xử lý cột thời gian ('Order date', 'SO_TIME').\")\n\n    # Xử lý giá trị thiếu (Missing Values)\n    cols_to_drop_due_to_nulls = ['QTUF_RCV_NO', 'SOUF_RCV_NO']\n    for col in cols_to_drop_due_to_nulls:\n        if col in train_df.columns:\n            train_df.drop(columns=[col], inplace=True)\n        if col in test_df.columns:\n            test_df.drop(columns=[col], inplace=True)\n    print(\"✅ Đã loại bỏ 'QTUF_RCV_NO' và 'SOUF_RCV_NO' (nếu tồn tại) do nhiều giá trị thiếu.\")\n\n    # Điền giá trị thiếu cho các cột còn lại\n    categorical_fillna_cols = ['REASON_CD', 'OTHER AREA SHIP DIV', 'Ship Mode']\n    for col in categorical_fillna_cols:\n        if col in train_df.columns:\n            train_df[col] = train_df[col].fillna('UNKNOWN')\n        if col in test_df.columns:\n            test_df[col] = test_df[col].fillna('UNKNOWN')\n\n    numerical_fillna_cols = ['SHIP DECISION NO', 'SUPPLIER_DIV']\n    for col in numerical_fillna_cols:\n        if col in train_df.columns:\n            median_val = train_df[col].median()\n            train_df[col] = train_df[col].fillna(median_val)\n            if col in test_df.columns:\n                test_df[col] = test_df[col].fillna(median_val)\n    print(\"✅ Đã xử lý các giá trị thiếu còn lại.\")\n\n    # Xử lý cột trùng lặp 'SPECIAL DIV' và 'SPECIAL_DIV'\n    if 'SPECIAL DIV' in train_df.columns and 'SPECIAL_DIV' in train_df.columns:\n        if (train_df['SPECIAL DIV'].astype(str).equals(train_df['SPECIAL_DIV'].astype(str))):\n            train_df.drop(columns=['SPECIAL DIV'], inplace=True)\n            if 'SPECIAL DIV' in test_df.columns:\n                test_df.drop(columns=['SPECIAL DIV'], inplace=True)\n            print(\"✅ Đã loại bỏ cột 'SPECIAL DIV' do trùng lặp với 'SPECIAL_DIV'.\")\n        else:\n            print(\"⚠️ Cột 'SPECIAL DIV' và 'SPECIAL_DIV' khác nhau, không loại bỏ.\")\n\n    # Mã hóa các cột phân loại (Categorical Encoding)\n    categorical_cols_in_train = train_df.select_dtypes(include='object').columns.tolist()\n    categorical_cols_to_encode_in_both = [col for col in categorical_cols_in_train if col in test_df.columns]\n\n    print(f\"Các cột phân loại được mã hóa (chung cho cả train và test): {categorical_cols_to_encode_in_both}\")\n\n    for col in categorical_cols_to_encode_in_both:\n        le = LabelEncoder()\n        all_values = pd.concat([train_df[col].astype(str), test_df[col].astype(str)]).unique()\n        le.fit(all_values)\n        train_df[col] = le.transform(train_df[col].astype(str))\n        test_df[col] = le.transform(test_df[col].astype(str))\n    print(\"✅ Đã mã hóa các cột phân loại chung bằng Label Encoding.\")\n\n    # Xử lý Outliers và Chuẩn hóa (Scaling)\n    skewed_cols = ['PURCHASE AMOUNT', 'SUPPLIER INV AMOUNT']\n    for col in skewed_cols:\n        if col in train_df.columns:\n            train_df[col] = np.log1p(train_df[col].clip(lower=0))\n            if col in test_df.columns:\n                test_df[col] = np.log1p(test_df[col].clip(lower=0))\n    print(\"✅ Đã áp dụng log1p cho 'PURCHASE AMOUNT', 'SUPPLIER INV AMOUNT' (nếu tồn tại).\\n\")\n\n    if 'WEIGHT PER PIECE' in train_df.columns:\n        cap_value = train_df['WEIGHT PER PIECE'].quantile(0.99)\n        train_df['WEIGHT PER PIECE'] = np.where(train_df['WEIGHT PER PIECE'] > cap_value, cap_value, train_df['WEIGHT PER PIECE'])\n        if 'WEIGHT PER PIECE' in test_df.columns:\n            test_df['WEIGHT PER PIECE'] = np.where(test_df['WEIGHT PER PIECE'] > cap_value, cap_value, test_df['WEIGHT PER PIECE'])\n    print(\"✅ Đã giới hạn outliers cho 'WEIGHT PER PIECE' (nếu tồn tại).\")\n\n    numerical_features_for_scaling = [\n        'PURCHASE AMOUNT', 'SUPPLIER INV AMOUNT', 'WEIGHT PER PIECE', 'PACK QTY', 'SO QTY',\n        'SO_DAY_OF_WEEK', 'SO_DAY_OF_MONTH', 'SO_HOUR', 'SO_MINUTE', 'SO_SECOND',\n        'order_year', 'order_month', 'order_day', 'order_dayofweek', 'order_is_weekend',\n        'SHIP DECISION NO', 'SUPPLIER_DIV'\n    ]\n    numerical_features_for_scaling = [col for col in numerical_features_for_scaling if col in train_df.columns]\n\n    scaler = StandardScaler()\n    train_df[numerical_features_for_scaling] = scaler.fit_transform(train_df[numerical_features_for_scaling])\n    test_df[numerical_features_for_scaling] = scaler.transform(test_df[numerical_features_for_scaling])\n    print(\"✅ Đã chuẩn hóa các đặc trưng số bằng StandardScaler.\")\n\n    # Đảm bảo tập đặc trưng là nhất quán giữa train và test\n    train_cols_set = set(train_df.columns)\n    test_cols_set = set(test_df.columns)\n\n    cols_to_exclude_train = {'label'}\n    cols_to_exclude_test = {'ID'}\n\n    final_train_features = list(train_cols_set - cols_to_exclude_train)\n    final_test_features = list(test_cols_set - cols_to_exclude_test)\n\n    common_features = sorted(list(set(final_train_features) & set(final_test_features)))\n\n    for col in final_train_features:\n        if col not in common_features:\n            if col in train_df.columns:\n                train_df.drop(columns=[col], inplace=True)\n                print(f\"⚠️ Loại bỏ cột '{col}' khỏi train_df vì không có trong test_df.\")\n            \n    for col in final_test_features:\n        if col not in common_features:\n            if col in test_df.columns:\n                test_df.drop(columns=[col], inplace=True)\n                print(f\"⚠️ Loại bỏ cột '{col}' khỏi test_df vì không có trong train_df.\")\n            \n    train_df = train_df[common_features + ['label']]\n    test_df = test_df[common_features + ['ID']]\n\n    print(f\"Kích thước tập huấn luyện sau tiền xử lý và đồng bộ cột: {train_df.shape}\")\n    print(f\"Kích thước tập kiểm tra sau tiền xử lý và đồng bộ cột: {test_df.shape}\")\n    return train_df, test_df\n\n# --- 3. Hàm huấn luyện mô hình với tối ưu hóa siêu tham số ---\ndef train_and_predict(train_df, test_df):\n    \"\"\"\n    Huấn luyện mô hình LightGBM với KFold Cross-Validation,\n    sử dụng SMOTE để xử lý mất cân bằng dữ liệu, và tối ưu ngưỡng.\n    \"\"\"\n    print(\"\\n--- Bắt đầu huấn luyện mô hình ---\\n\")\n\n    X = train_df.drop(columns=['label'])\n    y = train_df['label']\n    X_test = test_df.drop(columns=['ID'])\n\n    # Tính toán scale_pos_weight cho dữ liệu mất cân bằng (vẫn hữu ích ngay cả với SMOTE)\n    if 0 in y.value_counts() and 1 in y.value_counts():\n        neg_count = y.value_counts()[0]\n        pos_count = y.value_counts()[1]\n        scale_pos_weight_value = neg_count / pos_count\n        print(f\"Tính toán scale_pos_weight ban đầu: {scale_pos_weight_value:.2f}\")\n    else:\n        scale_pos_weight_value = 1.0\n        print(\"Cảnh báo: Chỉ có một lớp trong tập huấn luyện. Đặt scale_pos_weight = 1.0\")\n\n    # KHỐI CODE RandomizedSearchCV (GIỮ LẠI VÀ CHẠY VỚI GPU)\n    # LƯU Ý: Với n_jobs=1, quá trình tìm kiếm sẽ tuần tự nhưng mỗi lần huấn luyện sẽ nhanh hơn nhiều nhờ GPU.\n    param_dist = {\n        'n_estimators': [300, 500, 700, 1000],\n        'learning_rate': [0.005, 0.01, 0.05, 0.1],\n        'num_leaves': [20, 31, 40, 60, 80],\n        'max_depth': [-1, 5, 7, 9, 12],\n        'min_child_samples': [10, 20, 30, 40],\n        'subsample': [0.7, 0.8, 0.9, 1.0],\n        'colsample_bytree': [0.7, 0.8, 0.9, 1.0],\n        'reg_alpha': [0, 0.01, 0.1, 0.5, 1.0],\n        'reg_lambda': [0, 0.01, 0.1, 0.5, 1.0],\n        'scale_pos_weight': sorted(list(set([\n            scale_pos_weight_value * 0.5,\n            scale_pos_weight_value,\n            scale_pos_weight_value * 1.5,\n            scale_pos_weight_value * 2.0,\n            scale_pos_weight_value * 3.0,\n            scale_pos_weight_value * 5.0\n        ])))\n    }\n\n    # Khởi tạo LightGBM cho RandomizedSearchCV VỚI GPU\n    lgbm = lgb.LGBMClassifier(random_state=42, n_jobs=-1, objective='binary', device='gpu') \n    print(\"Đang tìm kiếm siêu tham số tốt nhất với RandomizedSearchCV (sử dụng KFold n_splits=3)...\")\n    rand_search = RandomizedSearchCV(\n        estimator=lgbm,\n        param_distributions=param_dist,\n        n_iter=5, # Số lần lặp tìm kiếm. Có thể tăng lên nếu muốn tìm kiếm kỹ hơn (sẽ mất thêm thời gian).\n        scoring='f1',\n        cv=KFold(n_splits=3, shuffle=True, random_state=42), # Giảm n_splits để tìm kiếm nhanh hơn\n        verbose=1,\n        random_state=42,\n        n_jobs=1 # ĐẶT n_jobs=1 ĐỂ TRÁNH LỖI RAM VÀ TRANH CHẤP GPU\n    )\n    rand_search.fit(X, y)\n    best_params = rand_search.best_params_\n    print(f\"✅ Siêu tham số tốt nhất tìm được: {best_params}\\n\")\n\n    kf = KFold(n_splits=5, shuffle=True, random_state=42)\n    oof_preds_proba = np.zeros(len(X))\n    test_preds_proba = np.zeros(len(X_test))\n\n    smote = SMOTE(random_state=42) # Đã loại bỏ n_jobs=-1 khỏi SMOTE\n    \n    print(\"Đang huấn luyện mô hình cuối cùng với KFold Cross-Validation (5 folds), SMOTE và siêu tham số tốt nhất...\")\n    for fold, (train_idx, val_idx) in enumerate(kf.split(X)):\n        print(f\"\\n--- Đang huấn luyện Fold {fold + 1} ---\")\n        X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]\n        y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]\n\n        print(f\"   Phân bố lớp ban đầu của Fold {fold + 1} (trước SMOTE):\")\n        print(y_train.value_counts())\n\n        # Áp dụng SMOTE CHỈ trên tập huấn luyện của fold hiện tại\n        print(f\"   Áp dụng SMOTE cho tập huấn luyện của Fold {fold + 1}...\")\n        X_train_resampled, y_train_resampled = smote.fit_resample(X_train, y_train)\n        print(f\"   Kích thước tập huấn luyện sau SMOTE: {X_train_resampled.shape}\")\n        print(f\"   Phân bố lớp sau SMOTE của Fold {fold + 1}:\")\n        print(y_train_resampled.value_counts())\n\n        # KHỞI TẠO MODEL CUỐI CÙNG VỚI GPU\n        model = lgb.LGBMClassifier(**best_params, random_state=42, n_jobs=-1, objective='binary', device='gpu') # THÊM 'device='gpu'' TẠI ĐÂY\n        model.fit(\n            X_train_resampled, y_train_resampled, # Huấn luyện trên dữ liệu đã được SMOTE\n            eval_set=[(X_val, y_val)], # Đánh giá trên dữ liệu validation gốc\n            eval_metric='f1',\n            callbacks=[lgb.early_stopping(100, verbose=100)]\n        )\n\n        oof_preds_proba[val_idx] = model.predict_proba(X_val)[:, 1]\n        test_preds_proba += model.predict_proba(X_test)[:, 1] / kf.n_splits\n    \n    # --- Tối ưu ngưỡng phân loại trên OOF predictions ---\n    precisions, recalls, thresholds = precision_recall_curve(y, oof_preds_proba)\n    f1_scores = 2 * (precisions * recalls) / (precisions + recalls + 1e-6)\n    \n    best_f1_idx = np.argmax(f1_scores)\n    best_threshold = thresholds[best_f1_idx]\n    \n    oof_preds_optimized = (oof_preds_proba >= best_threshold).astype(int)\n    f1_overall_optimized = f1_score(y, oof_preds_optimized)\n\n    print(f\"\\n✅ F1 Score trung bình trên tập huấn luyện (OOF) (ngưỡng 0.5): {f1_score(y, (oof_preds_proba >= 0.5).astype(int)):.4f}\")\n    print(f\"✅ Ngưỡng tối ưu tìm được trên OOF: {best_threshold:.4f}\")\n    print(f\"✅ F1 Score trung bình trên tập huấn luyện (OOF) (ngưỡng tối ưu): {f1_overall_optimized:.4f}\")\n\n    return test_preds_proba, best_threshold\n\n# --- 4. Hàm tạo file Submission ---\ndef create_submission_file(test_df_original, test_preds_proba, best_threshold, filename=\"submission.csv\"):\n    \"\"\"\n    Tạo file submission cuối cùng.\n    Sử dụng test_df_original để lấy cột 'ID'.\n    \"\"\"\n    print(\"\\n--- Tạo file Submission ---\")\n    # Áp dụng ngưỡng tối ưu đã tìm được\n    final_test_pred = (test_preds_proba >= best_threshold).astype(int)\n\n    submission = pd.DataFrame({\n        'ID': test_df_original['ID'],\n        'Predicted': final_test_pred\n    })\n    submission.to_csv(filename, index=False)\n    print(f\"✅ Đã tạo file submission: {filename}\")\n    print(submission.head())\n\n# --- Chạy toàn bộ quy trình ---\nif __name__ == \"__main__\":\n    # ĐẶT ĐƯỜNG DẪN ĐẾN THƯ MỤC CHỨA DỮ LIỆU CỦA BẠN TRÊN KAGGLE\n    # THAY THẾ 'ds-108-p-21-assigment-06' BẰNG TÊN CHÍNH XÁC CỦA DATASET CỦA BẠN TRÊN KAGGLE NẾU KHÁC\n    kaggle_data_path = \"/kaggle/input/ds-108-p-21-assigment-06\"\n\n    # 1. Tải dữ liệu\n    train_df, test_df_original = load_and_combine_data_kaggle(kaggle_data_path)\n\n    # 2. Tiền xử lý dữ liệu (truyền bản sao để không ảnh hưởng đến DataFrame gốc)\n    train_df_processed, test_df_processed = preprocess_data(train_df.copy(), test_df_original.copy())\n\n    # 3. Huấn luyện mô hình và dự đoán (nhận lại xác suất và ngưỡng tối ưu)\n    final_test_predictions_proba, optimal_threshold = train_and_predict(train_df_processed, test_df_processed)\n\n    # 4. Tạo file Submission (truyền cả xác suất và ngưỡng tối ưu)\n    create_submission_file(test_df_original, final_test_predictions_proba, optimal_threshold)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T18:22:13.111008Z","iopub.execute_input":"2025-06-17T18:22:13.111296Z","iopub.status.idle":"2025-06-17T18:45:04.411382Z","shell.execute_reply.started":"2025-06-17T18:22:13.111274Z","shell.execute_reply":"2025-06-17T18:45:04.410434Z"}},"outputs":[],"execution_count":null}]}