{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.11"},"colab":{"collapsed_sections":["Zlkrkwk5fMYa","Mhl5B4r8lbeF","GjvG4P5IQG_U","KiF3f1IPP3xb","vBg4clWgQCVC","wT0WIbaSQOla"],"provenance":[]},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":103436,"databundleVersionId":12476472,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12203969,"sourceType":"datasetVersion","datasetId":7680301}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<!-- Tiêu đề 1: Tiền xử lý và chuẩn hóa dữ liệu -->\n<div style=\"background: linear-gradient(to right, #ff9a9e, #fad0c4); padding: 16px; border-radius: 12px; text-align: center; font-size: 28px; font-weight: bold; color: #333;\">\n  🧹 Tiền xử lý và chuẩn hóa dữ liệu <br> <span style=\"font-size: 18px; font-weight: normal;\">Tạo Bronze, Silver Data</span>\n</div>\n\n<br>","metadata":{}},{"cell_type":"code","source":"# load các thư viện\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom scipy.stats import entropy, mode\nfrom sklearn.preprocessing import LabelEncoder\nfrom datetime import time","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:37.515035Z","iopub.execute_input":"2025-06-18T08:45:37.515428Z","iopub.status.idle":"2025-06-18T08:45:39.125370Z","shell.execute_reply.started":"2025-06-18T08:45:37.515391Z","shell.execute_reply":"2025-06-18T08:45:39.124013Z"},"id":"l3c246ZMTwTo","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Hiểu Dữ liệu","metadata":{}},{"cell_type":"markdown","source":"M company là một tập đoàn chuyên cung cấp sản phẩm theo đơn đặt hàng. Họ hoạt động như một **trung gian kết nối giữa nhà cung cấp và khách hàng cuối** .\n\n\n**Dữ liệu này phản ánh giao dịch giữa:**\n\n### 👉 M company và Nhà cung cấp (Supplier), chứ không phải với khách hàng cuối.\n\n### Lý do:\n\n* Có cột `SUPPLIER_CD`, `SUPPLIER_CATEGORY_CD`, `SUPPLIER_INV_AMOUNT` → thể hiện rõ đây là đơn mua từ **nhà cung cấp**.\n* Có cả `VSD` (Vendor Shipping Day) → ngày nhà cung cấp giao hàng cho M company.\n* Có `PRODUCT_ATTRIBUTION` mô tả sản phẩm là **có sẵn hay cần sản xuất** → gợi ý rằng M company **đặt mua hàng hóa từ đối tác để phân phối lại**.\n\n\n\n## 🌐 Vai trò của M company trong dữ liệu:\n\nM company ở đây giống như **một trung tâm phân phối (distributor)**:\n\n1. Đặt hàng từ nhà cung cấp.\n2. Lưu kho hoặc xử lý đơn.\n3. Giao lại cho khách hàng (chưa thể hiện trong dataset này).\n\n\n\n## 💡 Label “delay” là gì?\n\n→ Là độ trễ **trong việc nhận hàng từ nhà cung cấp** hoặc **sự chậm trễ trong việc đáp ứng thời gian giao hàng kỳ vọng (VSD)**.","metadata":{}},{"cell_type":"markdown","source":"# Data Processing 4 - 6","metadata":{"id":"nfQPWGIN_LGt"}},{"cell_type":"markdown","source":"Dữ liệu đã cho gồm có 2 bảng định dạng CSV và EXCEL\n\n- File `delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv` và `not_delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv` lưu các thông tin về các đơn hàng bị delay và KHÔNG bị delay từ tháng 4 -> 6. Sau khi nối lại với nhau gọi là df_full_4_6.\n\n- File `Sample codebook of Delay Prediction task.xlsx` lưu thông tin ý nghĩa các trường trong dataset. Sau đây gọi là codebook.\n\nTuy nhiên có 1 số điểm chưa được đồng bộ giữa dataset và codebook (codebook chưa mô tả đủ các trường), dễ gây hiểu lầm trong quá trình làm việc.","metadata":{"id":"hN988Duo_Qjy"}},{"cell_type":"markdown","source":"# 1. **Ingestion & Metadata Logging & BRONZE Layer Data**","metadata":{}},{"cell_type":"markdown","source":"## **1.1 Data Ingestion**","metadata":{}},{"cell_type":"code","source":"# Load data\nbase_path = \"/kaggle/input/ds-108-p-21-assigment-06\"\n\n# Đọc từng file CSV vào DataFrame\ndf_delay_4_6 = pd.read_csv(f\"{base_path}/delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv\", encoding='utf-8')\ndf_not_delay_4_6 = pd.read_csv(f\"{base_path}/not_delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv\", encoding='utf-8')\n\nprint(df_delay_4_6.shape)\nprint(df_not_delay_4_6.shape)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:39.127313Z","iopub.execute_input":"2025-06-18T08:45:39.127788Z","iopub.status.idle":"2025-06-18T08:45:43.092526Z","shell.execute_reply.started":"2025-06-18T08:45:39.127761Z","shell.execute_reply":"2025-06-18T08:45:43.090998Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Nối delay và not delay thành data hoàn chỉnh\ndf_full_4_6 = pd.concat([df_delay_4_6, df_not_delay_4_6], ignore_index=True)\nprint(df_full_4_6.shape)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:43.094068Z","iopub.execute_input":"2025-06-18T08:45:43.094800Z","iopub.status.idle":"2025-06-18T08:45:43.253589Z","shell.execute_reply.started":"2025-06-18T08:45:43.094742Z","shell.execute_reply":"2025-06-18T08:45:43.252715Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load codebook\ncodebook = pd.read_excel(\n    \"/kaggle/input/m-company-delay-prediction/Sample codebook of Delay Prediction task.xlsx\",\n    sheet_name=\"Columns Details\",\n    header=0,\n    index_col=0\n)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:43.255301Z","iopub.execute_input":"2025-06-18T08:45:43.255603Z","iopub.status.idle":"2025-06-18T08:45:43.995393Z","shell.execute_reply.started":"2025-06-18T08:45:43.255581Z","shell.execute_reply":"2025-06-18T08:45:43.994465Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"số thuộc tính được liệt kê trong codebook: {codebook.shape[0]}\")\nprint(f\"số thuộc tính của df_full_4_6: {df_full_4_6.shape[1]}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:43.996397Z","iopub.execute_input":"2025-06-18T08:45:43.996790Z","iopub.status.idle":"2025-06-18T08:45:44.002992Z","shell.execute_reply.started":"2025-06-18T08:45:43.996764Z","shell.execute_reply":"2025-06-18T08:45:44.001985Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"> Có thể suy luận rằng có 10 thuộc tính không được định nghĩa trong codebook. Tuy nhiên nếu khẳng định vừa rồi sẽ sai nếu:\n- 1 số thuộc tính trong codebook không hề có trong df_full_4_6 => có nhiều hơn 10 thuộc tính không được định nghĩa\n- 1 số thuộc tính trong codebook có thể định nghĩa cho nhiều trường trong df_full_4_6 => có ít hơn 10 thuộc tính không được định nghĩa\n\n**Phần `c. Kiểm soát schema đầu vào (tên cột, kiểu dữ liệu)` bên dưới sẽ kiểm tra vấn đề này**","metadata":{}},{"cell_type":"markdown","source":"### **a. Timestamp ingest**","metadata":{}},{"cell_type":"code","source":"from datetime import datetime\n\n# Ghi lại thời điểm ingest hiện tại\ningest_timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\nprint(\"Ingest timestamp:\", ingest_timestamp)\n\n# Option: gắn timestamp vào toàn bộ DataFrame\ncodebook['ingest_timestamp'] = ingest_timestamp\ndf_full_4_6['ingest_timestamp'] = ingest_timestamp","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.003965Z","iopub.execute_input":"2025-06-18T08:45:44.004429Z","iopub.status.idle":"2025-06-18T08:45:44.029204Z","shell.execute_reply.started":"2025-06-18T08:45:44.004387Z","shell.execute_reply":"2025-06-18T08:45:44.028228Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **b. Data version**","metadata":{}},{"cell_type":"code","source":"# Version theo thời gian ingest\ndata_version = \"v_\" + datetime.now().strftime(\"%Y_%m_%d_%H_%M\")\nprint(\"Data version:\", data_version)\n\n# Có thể thêm vào metadata hoặc tên file lưu\ncodebook['data_version'] = data_version\ndf_full_4_6['data_version'] = data_version","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.030067Z","iopub.execute_input":"2025-06-18T08:45:44.030540Z","iopub.status.idle":"2025-06-18T08:45:44.048816Z","shell.execute_reply.started":"2025-06-18T08:45:44.030515Z","shell.execute_reply":"2025-06-18T08:45:44.048038Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧾 **1.2 Metadata Logging**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra mối quan hệ dữ liệu (`df_full_4_6` và `codebook`)\n","metadata":{"id":"hKJ331JPVaPp"}},{"cell_type":"markdown","source":"\nTrước khi phân tích dữ liệu, việc đảm bảo rằng **tất cả biến (columns) trong `df_full_4_6` có được định nghĩa trong `codebook`** là rất quan trọng.  \n\n- **`codebook`**: Chứa danh sách các biến có ý nghĩa, đóng vai trò như một tài liệu tham chiếu về cấu trúc dữ liệu (metadata).  \n- **`df_full_4_6`**: Chứa dữ liệu thực tế được thu thập, tất cả các cột phải được định nghĩa trong `codebook` để đảm bảo tính đúng đắn, khách quan khi phân tích.  \n\nSau đây tiến hành kiểm tra xem có biến nào trong `df_full_4_6` nhưng bị thiếu trong `codebook` không? Điều này giúp:  \n✔️ Đảm bảo dữ liệu không bị thiếu sót trước khi phân tích.  \n✔️ Xác định các cột không cần thiết hoặc đã đặt sai tên.  \n✔️ Chuẩn hóa dữ liệu để đảm bảo tính nhất quán.  \n\n\n---","metadata":{"id":"eqSEa2AWkEXl"}},{"cell_type":"code","source":"def check_variables_in_information(codebook, df_full_7_9):\n    \"\"\"\n    Kiểm tra trong df_full_7_9 có bao nhiêu biến được định nghĩa trong codebook,\n    và bao nhiêu biến không được định nghĩa (unexpected), bỏ qua hai cột kỹ thuật.\n\n    Args:\n    - codebook: DataFrame chứa danh sách các biến hợp lệ trong cột 'Attributions'\n    - df_full_7_9: DataFrame chứa dữ liệu thực tế\n\n    Returns:\n    - None (chỉ in ra thông tin chi tiết)\n    \"\"\"\n    # Tập biến hợp lệ đã định nghĩa\n    defined_variables = set(codebook[\"Attributions\"].str.strip())\n\n    # Các cột cần bỏ qua trong quá trình kiểm tra\n    ignored_columns = {\"data_version\", \"ingest_timestamp\"}\n\n    # Tập biến thực tế trong dữ liệu (sau khi loại bỏ các cột bị bỏ qua)\n    info_variables = set(df_full_7_9.columns.str.strip()) - ignored_columns\n\n    # Biến có trong df_full_7_9 mà cũng được định nghĩa\n    matched_variables = info_variables & defined_variables\n\n    # Biến có trong df_full_7_9 nhưng KHÔNG được định nghĩa\n    unexpected_variables = info_variables - defined_variables\n\n    print(f\"Tổng số biến trong df_full_7_9 (bỏ qua cột kỹ thuật): {len(info_variables)}\")\n    print(f\"Số biến được định nghĩa trong codebook: {len(matched_variables)}\")\n    print(f\"Số biến KHÔNG được định nghĩa trong codebook: {len(unexpected_variables)}\")\n\n    print(\"\\nDanh sách biến ĐƯỢC định nghĩa:\")\n    print(sorted(matched_variables))\n\n    if unexpected_variables:\n        print(\"\\n🔺 Danh sách biến KHÔNG ĐƯỢC định nghĩa:\")\n        print(sorted(unexpected_variables))\n    else:\n        print(\"\\n🎉 Không có biến nào bất ngờ! Mọi thứ đều đúng chuẩn định nghĩa.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.049773Z","iopub.execute_input":"2025-06-18T08:45:44.050168Z","iopub.status.idle":"2025-06-18T08:45:44.062225Z","shell.execute_reply.started":"2025-06-18T08:45:44.050136Z","shell.execute_reply":"2025-06-18T08:45:44.061292Z"},"id":"2R-5KFWURq82","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_variables_in_information(codebook, df_full_4_6)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.063253Z","iopub.execute_input":"2025-06-18T08:45:44.063573Z","iopub.status.idle":"2025-06-18T08:45:44.083952Z","shell.execute_reply.started":"2025-06-18T08:45:44.063549Z","shell.execute_reply":"2025-06-18T08:45:44.082005Z"},"id":"Klizg7tZhXcu","outputId":"0890e85a-3721-4226-ffcd-fd63769e39a0","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Kiểm tra lại xem các biến đã định nghĩa trong `df_full_4_6` nhưng không có trong `codebook` có logic khác không**","metadata":{"id":"9yay5R5he4V8"}},{"cell_type":"markdown","source":"I. Các cột bị sai chính tả, tên không khớp\n\n> **Không có**\n\n---\n\nII. Các cột không có định nghĩa nhưng có thể suy luận từ định nghĩa các cột khác\n\n```yaml\n- name: LOGICAL_PLANT\n  type: int64\n  description: Mã “kho logic” trong ERP/WMS – phân vùng vận hành, vùng miền, hoặc theo khách hàng\n```\n\n---\n\nIII. Các cột không có định nghĩa và không thể suy ra từ định nghĩa các cột khác → Dựa vào tên cột và domain để đoán\n\n```yaml\n- Attributions: ACTUAL_SHIP_DAYS\n  Data Type: int64\n  Group Attributions: Thông tin vận chuyển\n  Description: Số ngày làm việc thực tế từ thời điểm đặt hàng đến ngày dự kiến giao (VSD)\n\n- Attributions: EXPENSIVE_FLG\n  Data Type: int64\n  Group Attributions: Thông tin sản phẩm\n  Description: Cờ đánh dấu mặt hàng có giá trị cao (1: đắt | 0: không đắt)\n\n- Attributions: HAZARD_FLG\n  Data Type: int64\n  Group Attributions: Thông tin sản phẩm\n  Description: Cờ đánh dấu mặt hàng nguy hiểm (1: có nguy cơ | 0: an toàn bình thường)\n\n- Attributions: HEAVY_FLG\n  Data Type: int64\n  Group Attributions: Thông tin sản phẩm\n  Description: Cờ đánh dấu mặt hàng nặng (1: nặng | 0: nhẹ)\n\n- Attributions: IO_UNFIT_FLG\n  Data Type: int64\n  Group Attributions: Mã lý do và lỗi\n  Description: Cờ đánh dấu “unfit” nội bộ khi giao dịch liên công ty, tự động loại bỏ các dòng hàng không phù hợp\n\n- Attributions: PRODUCT_ASSORT\n  Data Type: object\n  Group Attributions: Thông tin sản phẩm\n  Description: Loại sản phẩm, phân nhóm theo tính chất và dòng hàng\n\n- Attributions: SPECIFY_PRODUCTION_DAYS\n  Data Type: int64\n  Group Attributions: Thông tin sản phẩm\n  Description: Số ngày sản xuất được chỉ định bởi bộ phận front desk (chỉ set khi có yêu cầu đặc biệt)\n\n- Attributions: SPECIFY_SHIP_DAYS\n  Data Type: int64\n  Group Attributions: Thông tin vận chuyển\n  Description: Số ngày vận chuyển được chỉ định bởi bộ phận front desk (chỉ set khi có yêu cầu đặc biệt)\n\n- Attributions: SUPPLIER_CATEGORY_CD\n  Data Type: int64\n  Group Attributions: Thông tin nhà cung cấp\n  Description: Mã loại Supplier (phân nhóm nhà cung cấp theo danh mục nội bộ)\n\n- Attributions: WEIGHT_UNIT\n  Data Type: object\n  Group Attributions: Thông tin sản phẩm\n  Description: Đơn vị khối lượng (g, kg…)\n```","metadata":{"id":"GXkUwzcQXffN"}},{"cell_type":"markdown","source":"### b. Hoàn thiện codebook","metadata":{}},{"cell_type":"code","source":"# Tạo DataFrame chứa 10 dòng mới\nnew_rows = pd.DataFrame([\n    {\n        'Attributions': 'ACTUAL_SHIP_DAYS',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin vận chuyển',\n        'Description': 'Số ngày làm việc thực tế từ thời điểm đặt hàng đến ngày dự kiến giao (VSD)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'EXPENSIVE_FLG',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Cờ đánh dấu mặt hàng có giá trị cao (1: đắt | 0: không đắt)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'HAZARD_FLG',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Cờ đánh dấu mặt hàng nguy hiểm (1: có nguy cơ | 0: an toàn bình thường)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'HEAVY_FLG',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Cờ đánh dấu mặt hàng nặng (1: nặng | 0: nhẹ)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'IO_UNFIT_FLG',\n        'Data Type': 'int64',\n        'Group Attributions': 'Mã lý do và lỗi',\n        'Description': 'Cờ đánh dấu “unfit” nội bộ khi giao dịch liên công ty, tự động loại bỏ các dòng hàng không phù hợp',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'PRODUCT_ASSORT',\n        'Data Type': 'object',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Loại sản phẩm, phân nhóm theo tính chất và dòng hàng',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'SPECIFY_PRODUCTION_DAYS',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Số ngày sản xuất được chỉ định bởi bộ phận front desk (chỉ set khi có yêu cầu đặc biệt)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'SPECIFY_SHIP_DAYS',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin vận chuyển',\n        'Description': 'Số ngày vận chuyển được chỉ định bởi bộ phận front desk (chỉ set khi có yêu cầu đặc biệt)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'SUPPLIER_CATEGORY_CD',\n        'Data Type': 'int64',\n        'Group Attributions': 'Thông tin nhà cung cấp',\n        'Description': 'Mã loại Supplier (phân nhóm nhà cung cấp theo danh mục nội bộ)',\n        'Miền giá trị': None\n    },\n    {\n        'Attributions': 'WEIGHT_UNIT',\n        'Data Type': 'object',\n        'Group Attributions': 'Thông tin sản phẩm',\n        'Description': 'Đơn vị khối lượng (g, kg…)',\n        'Miền giá trị': None\n    }\n])\n\n# Tạo codebook_full = codebook cũ + 10 dòng mới\ncodebook_full = pd.concat([codebook, new_rows], ignore_index=True)\n\n# Giữ lại đúng 5 cột mong muốn\ncodebook_full = codebook_full[['Attributions', 'Data Type', 'Group Attributions', 'Description', 'Miền giá trị']]\n\n# Kiểm tra và Chuẩn hóa kiểu dữ liệu trong Codebook cho phù hợp\n\n# Cập nhật giá trị trong cột 'Data Type' nếu Attributions là order date hoặc 'VSD' \ncodebook_full.loc[codebook_full['Attributions'] == 'VSD', 'Data Type'] = 'DateTime'\ncodebook_full.loc[codebook_full['Attributions'] == 'Order date', 'Data Type'] = 'DateTime'\n\n# Xuất ra file Excel\ncodebook_full.sort_values(by='Group Attributions', ascending=False).to_excel('codebook_full_final.xlsx', index=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.087160Z","iopub.execute_input":"2025-06-18T08:45:44.087782Z","iopub.status.idle":"2025-06-18T08:45:44.147945Z","shell.execute_reply.started":"2025-06-18T08:45:44.087754Z","shell.execute_reply":"2025-06-18T08:45:44.147041Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"codebook_full.tail(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.149001Z","iopub.execute_input":"2025-06-18T08:45:44.149322Z","iopub.status.idle":"2025-06-18T08:45:44.171087Z","shell.execute_reply.started":"2025-06-18T08:45:44.149294Z","shell.execute_reply":"2025-06-18T08:45:44.170272Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔎 **1.3 Data Lineage**","metadata":{}},{"cell_type":"markdown","source":"### 🌟 Tổng quan về Lineage của project","metadata":{}},{"cell_type":"markdown","source":"| Thành phần | Chi tiết |\n|------------|----------|\n| 🎓 **Nguồn dữ liệu** | - `delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv` và `not_delay_4_6_CONDITION_PRODUCT_SUPPLIER.csv`<br>- `Sample codebook of Delay Prediction task.xlsx`<br><br>👉 Được cung cấp bởi Thầy Trần Quốc Khánh – Trường Đại học CNTT, ĐHQG-HCM |\n| 📥 **Data Ingestion** | - Thêm cột `Timestamp_Ingest` để đánh dấu thời gian ingestion<br>- Thêm cột `Data version` để theo dõi phiên bản |\n| 🛠️ **Schema Reconstruct** | - Thêm các tên biến chưa được định nghĩa vào codebook <br>- Xây dựng **codebook_full** ở định dạng excel với đầy đủ định nghĩa cột |\n| 📃 **Data Contracts** | - Thiết lập thỏa thuận sử dụng dữ liệu  |","metadata":{}},{"cell_type":"markdown","source":"## 🔐 **1.4 Governance & Access Control**","metadata":{}},{"cell_type":"markdown","source":"### 🧭 4 trụ cột vàng trong Governance của project","metadata":{}},{"cell_type":"markdown","source":"|Trụ cột|Mô tả|Ứng dụng trong project này|\n|---|---|---|\n|**Data Ownership**|Người “chính chủ” của dữ liệu|**Thầy Trần Quốc Khánh** – giảng viên hướng dẫn, người chịu trách nhiệm chính|\n|**Data Stewardship**|Người trực tiếp làm việc và bảo trì dữ liệu|**Lương Đắc Nguyên** – sinh viên thực hành, xử lý và ghi chú biến đổi|\n|**Data Quality**|Dữ liệu có sạch, đúng, đầy đủ?|Dữ liệu được chuẩn hóa tên thuộc tính, đã ghi lại version, timestamp nhưng chưa xử lý thiếu, giá trị trùng lặp|\n|**Data Policy & Rules**|Luật lệ rõ ràng trong sử dụng dữ liệu|Cam kết sử dụng cho học tập, không chia sẻ ngoài phạm vi lớp học|","metadata":{}},{"cell_type":"markdown","source":"### 🛡️ Access Control","metadata":{}},{"cell_type":"markdown","source":"|Vai trò|Quyền hạn|Ứng dụng thực tế|\n|---|---|---|\n|**Owner (Thầy Khánh)**|Toàn quyền sở hữu, cấp quyền, giám sát|Được cấp phát dữ liệu gốc & thiết lập chính sách|\n|**Steward (Sinh viên - Nguyên)**|Đọc & chỉnh sửa dữ liệu local|Toàn quyền xử lý, nhưng **không công khai**, không thay đổi schema tùy tiện|\n|**Khác (bên ngoài)**|Không có quyền truy cập|Không được xem hay sử dụng nếu không có sự cho phép|","metadata":{}},{"cell_type":"markdown","source":"### 📜 Data Contract ","metadata":{}},{"cell_type":"markdown","source":"📝 Cả hai bên đã thống nhất cam kết sau:\n\n- ✅ **Chỉ sử dụng dữ liệu cho học tập**\n- ✅ **Không chia sẻ công khai hoặc phát tán**\n- ✅ **Giữ nguyên cấu trúc schema, không tự ý chỉnh sửa**\n- ✅ **Có ghi lại mọi bước xử lý trong Data Lineage (phần 1.3)**","metadata":{}},{"cell_type":"markdown","source":"## 🥉**1.5 BRONZE Layer Data**","metadata":{}},{"cell_type":"code","source":"import os\n\noutput_dir = 'Data for practice'\nos.mkdir(output_dir)\n\noutput_path = os.path.join(output_dir, 'df_full_4_6.csv')\ndf_full_4_6.to_csv(output_path, index=False)\n\nprint(f\"📄 File CSV đã được lưu tại: {output_path}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:44.171928Z","iopub.execute_input":"2025-06-18T08:45:44.172180Z","iopub.status.idle":"2025-06-18T08:45:52.068849Z","shell.execute_reply.started":"2025-06-18T08:45:44.172159Z","shell.execute_reply":"2025-06-18T08:45:52.068013Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Sau khi hoàn thành các bước chuẩn bị ban đầu, ta đã xây dựng thành công lớp dữ liệu **Bronze** với:\n\n- **Dữ liệu thô:** `delay_full_4_6.csv`  \n- **Tài liệu mô tả (codebook):** `codebook_full.xlsx`\n\n📌 *Lưu ý:* Dữ liệu thô hiện tại vẫn **chưa được làm sạch hay chuẩn hóa**, chỉ là dạng dữ liệu gốc ban đầu.\n","metadata":{}},{"cell_type":"markdown","source":"# **2. Data Validation & Profiling**","metadata":{"id":"S47UZE6zmMZ1"}},{"cell_type":"markdown","source":"## **🧭 2.1 Data Profiling**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra kiểu dữ liệu","metadata":{}},{"cell_type":"code","source":"# In ra tên cột và dtype tương ứng trong DataFrame df_full_4_6\ncodebook_full[['Attributions', 'Data Type']].head()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:52.069851Z","iopub.execute_input":"2025-06-18T08:45:52.070208Z","iopub.status.idle":"2025-06-18T08:45:52.080210Z","shell.execute_reply.started":"2025-06-18T08:45:52.070180Z","shell.execute_reply":"2025-06-18T08:45:52.079047Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def convert_column_dtypes_from_codebook(df: pd.DataFrame, codebook: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    Chuyển đổi kiểu dữ liệu của các cột trong DataFrame đầu vào dựa trên định nghĩa từ codebook.\n\n    Args:\n        df (pd.DataFrame): DataFrame chứa dữ liệu gốc cần chuyển kiểu dữ liệu (vd: df_full_4_6).\n        codebook (pd.DataFrame): DataFrame định nghĩa kiểu dữ liệu cho từng cột, \n            yêu cầu có các cột:\n                - 'Attributions': tên cột trong df\n                - 'Data Type': kiểu dữ liệu đích (int64, float, object, DateTime...)\n\n    Returns:\n        pd.DataFrame: DataFrame đầu vào sau khi đã cố gắng chuyển đổi kiểu dữ liệu các cột tương ứng.\n\n    In ra:\n        - ✅ Với mỗi cột đổi thành công → có thể in ra tên cột và kiểu đã chuyển\n        - ❌ Với mỗi cột lỗi → in ra tên cột, kiểu định chuyển, và lý do lỗi (exception message)\n\n    Notes:\n        - Các kiểu dữ liệu hỗ trợ gồm: int, float, object/string, DateTime\n        - Với kiểu int sử dụng kiểu nullable 'Int64' của pandas để giữ lại giá trị NaN nếu có\n        - Nếu kiểu dữ liệu không được nhận diện hoặc lỗi ép kiểu → cột sẽ bị bỏ qua\n\n    Ví dụ dùng:\n        df_full_4_6 = convert_column_dtypes_from_codebook(df_full_4_6, codebook_full)\n    \"\"\"\n    for _, row in codebook.iterrows():\n        col = row['Attributions']\n        dtype = row['Data Type']\n\n        try:\n            if dtype.lower() in ['int', 'int64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('Int64')\n            elif dtype.lower() in ['float', 'float64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('float')\n            elif dtype.lower() in ['object', 'string']:\n                df[col] = df[col].astype(str)\n            elif dtype.lower() in ['datetime', 'datetime64', 'datetime64[ns]']:\n                try:\n                    # Cố gắng parse kiểu ISO8601 trước\n                    df[col] = pd.to_datetime(df[col], format='ISO8601', errors='raise')\n                    print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (ISO8601)\")\n                except Exception as e_iso:\n                    try:\n                        # Cố gắng với format kiểu Excel: \"4/1/2022  12:00:00 AM\"\n                        df[col] = pd.to_datetime(df[col], format=\"%m/%d/%Y %I:%M:%S %p\", errors='raise')\n                        print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (Excel-style)\")\n                    except Exception as e_excel:\n                        try:\n                            # Cuối cùng là mixed\n                            df[col] = pd.to_datetime(df[col], format='mixed', errors='raise')\n                            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (mixed format)\")\n                        except Exception as e_mixed:\n                            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → DateTime | ISO8601: {e_iso} | Excel: {e_excel} | mixed: {e_mixed}\")\n\n            else:\n                print(f\"Không rõ kiểu dữ liệu '{dtype}' cho cột '{col}', bỏ qua.\")\n                continue\n\n            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → {dtype}\")\n        \n        except Exception as e:\n            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → {dtype} | Lý do: {e}\")\n\n    return df","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:52.081244Z","iopub.execute_input":"2025-06-18T08:45:52.081568Z","iopub.status.idle":"2025-06-18T08:45:52.103649Z","shell.execute_reply.started":"2025-06-18T08:45:52.081541Z","shell.execute_reply":"2025-06-18T08:45:52.102522Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6 = convert_column_dtypes_from_codebook(df_full_4_6, codebook_full)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:52.104339Z","iopub.execute_input":"2025-06-18T08:45:52.104605Z","iopub.status.idle":"2025-06-18T08:45:52.729244Z","shell.execute_reply.started":"2025-06-18T08:45:52.104584Z","shell.execute_reply":"2025-06-18T08:45:52.728223Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":">Ta thấy có 1 số trường không áp dụng được kiểu dữ liệu của codebook, sau đây ta sẽ điều tra nguyên nhân và xử lý","metadata":{}},{"cell_type":"markdown","source":"`OTHER AREA SHIP DIV` → float64 | Lý do: Unable to parse string \" \" at position 3 \n\nPhân khu vận chuyển:\n- Khoảng trắng: Vận chuyển trong khu vực riêng\n- '1': Vận chuyển đến khu vực khác","metadata":{}},{"cell_type":"markdown","source":"Do không có kí tự nên `Khoảng trắng` có thể bị biểu diễn là null và bị `missing values`\nĐể đảm bào tính toàn vẹn của dữ liệu, ta quy ước `0` là kí tự thay thế cho `khoảng trắng` để chỉ Own area shipment","metadata":{}},{"cell_type":"code","source":"df_full_4_6['OTHER AREA SHIP DIV'] = df_full_4_6['OTHER AREA SHIP DIV'].replace(' ', 0).fillna(0)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:52.731415Z","iopub.execute_input":"2025-06-18T08:45:52.731697Z","iopub.status.idle":"2025-06-18T08:45:52.799565Z","shell.execute_reply.started":"2025-06-18T08:45:52.731676Z","shell.execute_reply":"2025-06-18T08:45:52.798615Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`Consider count hodiday Saturday` → int64 | Lý do: Unable to parse string \" \" at position 170932\n\nTa nhận thấy rằng trong dữ liệu chỉ có 1 dòng bị missing, đồng thời dòng đó có nhãn là `label = 0`. Do đó việc bỏ dòng này là lựa chọn tốt nhất.\n\n![image.png](attachment:image.png)","metadata":{},"attachments":{"image.png":{"image/png":"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"}}},{"cell_type":"code","source":"df_full_4_6 = df_full_4_6.drop(df_full_4_6.index[170932])","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:52.800505Z","iopub.execute_input":"2025-06-18T08:45:52.800738Z","iopub.status.idle":"2025-06-18T08:45:53.116754Z","shell.execute_reply.started":"2025-06-18T08:45:52.800720Z","shell.execute_reply":"2025-06-18T08:45:53.115317Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`REASON_CD` → float64 \n\n>Lý do bị lỗi khi chuyển đổi: Không thể parse chuỗi \" \" ở vị trí dòng thứ 4.\n>\n>Ý nghĩa cột: Mã lý do đơn hàng bị delay.\n\n>Nhận định: Những dòng có chứa giá trị thường ứng với label = 1, tức đơn hàng bị giao trễ. Điều này cho thấy REASON_CD là một thông tin chỉ xuất hiện sau khi đơn hàng đã bị delay, hay nói cách khác là dữ liệu thuộc tương lai so với thời điểm ta cần dự đoán.\n\n>Hành động: Vì không thể sử dụng dữ liệu tương lai trong quá trình huấn luyện mô hình (training), nên cột REASON_CD sẽ bị loại bỏ khỏi dataset.","metadata":{}},{"cell_type":"code","source":"print(df_full_4_6['REASON_CD'].unique())  ","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:53.118295Z","iopub.execute_input":"2025-06-18T08:45:53.118676Z","iopub.status.idle":"2025-06-18T08:45:53.143654Z","shell.execute_reply.started":"2025-06-18T08:45:53.118645Z","shell.execute_reply":"2025-06-18T08:45:53.142517Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Nếu muốn convert cột sang float (cẩn thận lỗi), có thể làm sạch trước:\ndf_full_4_6['REASON_CD'] = df_full_4_6['REASON_CD'].replace(r'^\\s*$', pd.NA, regex=True)  # thay chuỗi trống/cách bằng NA\n\n# Tuy nhiên vì cột này là dữ liệu tương lai, ta sẽ loại bỏ:\ndf_full_4_6 = df_full_4_6.drop(columns=['REASON_CD'])  # loại bỏ hoàn toàn cột REASON_CD\ncodebook_full = codebook_full[codebook_full['Attributions'] != 'REASON_CD']","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:53.144524Z","iopub.execute_input":"2025-06-18T08:45:53.144988Z","iopub.status.idle":"2025-06-18T08:45:53.463274Z","shell.execute_reply.started":"2025-06-18T08:45:53.144956Z","shell.execute_reply":"2025-06-18T08:45:53.462470Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6 = convert_column_dtypes_from_codebook(df_full_4_6, codebook_full)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:53.464311Z","iopub.execute_input":"2025-06-18T08:45:53.464653Z","iopub.status.idle":"2025-06-18T08:45:54.027261Z","shell.execute_reply.started":"2025-06-18T08:45:53.464624Z","shell.execute_reply":"2025-06-18T08:45:54.026467Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Phân rã thuộc tính kiểu datetime, time**","metadata":{}},{"cell_type":"code","source":"# Tách Order date\ndf_full_4_6['order_year'] = df_full_4_6['Order date'].dt.year\ndf_full_4_6['order_month'] = df_full_4_6['Order date'].dt.month\ndf_full_4_6['order_dayofweek'] = df_full_4_6['Order date'].dt.dayofweek\ndf_full_4_6['is_order_weekend'] = df_full_4_6['order_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tách VSD\ndf_full_4_6['vsd_year'] = df_full_4_6['VSD'].dt.year\ndf_full_4_6['vsd_month'] = df_full_4_6['VSD'].dt.month\ndf_full_4_6['vsd_dayofweek'] = df_full_4_6['VSD'].dt.dayofweek\ndf_full_4_6['is_vsd_weekend'] = df_full_4_6['vsd_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tính khoảng cách giữa VSD và Order date (nếu hợp lý về logic nghiệp vụ)\ndf_full_4_6['days_to_vsd'] = (df_full_4_6['VSD'] - df_full_4_6['Order date']).dt.days","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.028125Z","iopub.execute_input":"2025-06-18T08:45:54.028355Z","iopub.status.idle":"2025-06-18T08:45:54.439330Z","shell.execute_reply.started":"2025-06-18T08:45:54.028337Z","shell.execute_reply":"2025-06-18T08:45:54.437981Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Ta chuyển kiểu dữ liệu 1 số trường là khóa ngoại về `object`**","metadata":{}},{"cell_type":"code","source":"cols_to_convert = ['SUPPLIER_CATEGORY_CD', 'CLASSIFY_CD', 'CUST_CD', 'SHIP DECISION NO']\ndf_full_4_6[cols_to_convert] = df_full_4_6[cols_to_convert].astype('object')","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.440665Z","iopub.execute_input":"2025-06-18T08:45:54.441033Z","iopub.status.idle":"2025-06-18T08:45:54.522008Z","shell.execute_reply.started":"2025-06-18T08:45:54.441002Z","shell.execute_reply":"2025-06-18T08:45:54.521029Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic của 1 số cột về missing values","metadata":{}},{"cell_type":"code","source":"df_full_4_6['SOUF_RCV_NO'].value_counts()[:3]","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.522954Z","iopub.execute_input":"2025-06-18T08:45:54.523271Z","iopub.status.idle":"2025-06-18T08:45:54.573496Z","shell.execute_reply.started":"2025-06-18T08:45:54.523242Z","shell.execute_reply":"2025-06-18T08:45:54.572616Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6['Ship Mode'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.574515Z","iopub.execute_input":"2025-06-18T08:45:54.574965Z","iopub.status.idle":"2025-06-18T08:45:54.611521Z","shell.execute_reply.started":"2025-06-18T08:45:54.574930Z","shell.execute_reply":"2025-06-18T08:45:54.610511Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy cột `SOUF_RCV_NO` và `Ship Mode` mặc dù đa phần giá trị là `nan` nhưng do cột đang kiểu object nên bị hiểu lầm là chuỗi string. Nên ta sẽ chuyển nó về kiểu float và thay giá trị chuỗi `nan` thành `NAN`","metadata":{}},{"cell_type":"code","source":"# Thay các chuỗi 'nan' (chuỗi chữ, không phải NaN thật sự) thành np.nan\ndf_full_4_6['SOUF_RCV_NO'] = df_full_4_6['SOUF_RCV_NO'].replace('nan', np.nan)\ndf_full_4_6['Ship Mode'] = df_full_4_6['Ship Mode'].replace('nan', np.nan)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.612527Z","iopub.execute_input":"2025-06-18T08:45:54.612814Z","iopub.status.idle":"2025-06-18T08:45:54.722476Z","shell.execute_reply.started":"2025-06-18T08:45:54.612792Z","shell.execute_reply":"2025-06-18T08:45:54.721483Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic ẩn của 1 số cột","metadata":{}},{"cell_type":"markdown","source":"🔧 Vấn đề: Cột SO_TIME (Giờ đặt hàng) hiện đang được mã hóa sai kiểu dữ liệu – dưới dạng số nguyên (int), `ví dụ: 115325 → thực chất là 11:53:25`\n\n\nTa cần chuyển đổi các giá trị số này về kiểu thời gian chuẩn (datetime.time) để xử lý chính xác hơn ở các bước phân tích và trích xuất đặc trưng.","metadata":{}},{"cell_type":"code","source":"def convert_to_time(x):\n    try:\n        x_str = str(int(x)).zfill(6)\n        hour = int(x_str[:2])\n        minute = int(x_str[2:4])\n        second = int(x_str[4:6])\n        return time(hour, minute, second)\n    except:\n        return pd.NaT\n\n# Áp dụng\ndf_full_4_6['SO_TIME'] = df_full_4_6['SO_TIME'].apply(convert_to_time)\n\n\n# Kiểm tra kết quả\nprint(\"✅ Đã chuẩn hóa cột 'SO_TIME' về kiểu thời gian thực tế.\")\ndf_full_4_6['SO_TIME'].head()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:54.723761Z","iopub.execute_input":"2025-06-18T08:45:54.724129Z","iopub.status.idle":"2025-06-18T08:45:55.186234Z","shell.execute_reply.started":"2025-06-18T08:45:54.724099Z","shell.execute_reply":"2025-06-18T08:45:55.185361Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đảm bảo cột SO_TIME đã là kiểu datetime.time\n# Nếu chưa: df_full_4_6['SO_TIME'] = df_full_4_6['SO_TIME'].apply(convert_to_time)\n\n# Tạo đặc trưng từ SO_TIME\ndf_full_4_6['so_hour'] = df_full_4_6['SO_TIME'].apply(lambda x: x.hour if pd.notnull(x) else np.nan)\ndf_full_4_6['so_minute'] = df_full_4_6['SO_TIME'].apply(lambda x: x.minute if pd.notnull(x) else np.nan)\ndf_full_4_6['so_second'] = df_full_4_6['SO_TIME'].apply(lambda x: x.second if pd.notnull(x) else np.nan)\n\n# Buổi sáng hay chiều\ndf_full_4_6['is_morning'] = df_full_4_6['so_hour'].apply(lambda x: 1 if x < 12 else 0 if pd.notnull(x) else np.nan)\n\n# Gán time slot theo khung giờ sinh hoạt\ndef assign_time_slot(hour):\n    if pd.isnull(hour): return np.nan\n    elif 0 <= hour < 6: return 'midnight'\n    elif 6 <= hour < 12: return 'morning'\n    elif 12 <= hour < 18: return 'afternoon'\n    else: return 'evening'\n\ndf_full_4_6['time_slot'] = df_full_4_6['so_hour'].apply(assign_time_slot)\n\n# Binned theo khung 4 tiếng\ndf_full_4_6['time_bin'] = pd.cut(df_full_4_6['so_hour'],\n                                 bins=[-1, 3, 7, 11, 15, 19, 23],\n                                 labels=['0-3h', '4-7h', '8-11h', '12-15h', '16-19h', '20-23h'])\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:55.187263Z","iopub.execute_input":"2025-06-18T08:45:55.187578Z","iopub.status.idle":"2025-06-18T08:45:56.459098Z","shell.execute_reply.started":"2025-06-18T08:45:55.187553Z","shell.execute_reply":"2025-06-18T08:45:56.458253Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### b. Thông tin missing và duplicate trên toàn bộ tập dữ liệu","metadata":{}},{"cell_type":"code","source":"def print_data_overview(df, columns=None):\n    if columns is not None:\n        df = df[columns]\n\n    num_rows = df.shape[0]\n    num_cols = df.shape[1]\n    total_cells = num_rows * num_cols\n    percent_missing = df.isnull().sum().sum() / total_cells * 100\n    percent_duplicate = df.duplicated().sum() / num_rows * 100\n\n    print(f\"Tổng quan: {num_rows} dòng, {num_cols} cột\")\n    print(f\"⚠️  {percent_missing:.4f}% missing | 🔁 {percent_duplicate:.4f}% duplicate\\n\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:56.464884Z","iopub.execute_input":"2025-06-18T08:45:56.465202Z","iopub.status.idle":"2025-06-18T08:45:56.470829Z","shell.execute_reply.started":"2025-06-18T08:45:56.465179Z","shell.execute_reply":"2025-06-18T08:45:56.469904Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_full_4_6)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:56.471690Z","iopub.execute_input":"2025-06-18T08:45:56.471957Z","iopub.status.idle":"2025-06-18T08:45:58.217024Z","shell.execute_reply.started":"2025-06-18T08:45:56.471935Z","shell.execute_reply":"2025-06-18T08:45:58.216076Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### c. Các giá trị thống kê, đo lường độ Trung tâm và Phân Tán của dữ liệu","metadata":{}},{"cell_type":"markdown","source":">**Danh sách các cột mang ý nghĩa thống kê (Không bao gồm khóa ngoại, khóa chính, code), lưu ý kiểu dữ liệu của một số cột đã được xử lý thủ công**","metadata":{}},{"cell_type":"code","source":"print(df_full_4_6.select_dtypes(exclude='object').columns)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:58.217911Z","iopub.execute_input":"2025-06-18T08:45:58.218181Z","iopub.status.idle":"2025-06-18T08:45:58.381222Z","shell.execute_reply.started":"2025-06-18T08:45:58.218154Z","shell.execute_reply":"2025-06-18T08:45:58.380342Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6.describe()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:58.382273Z","iopub.execute_input":"2025-06-18T08:45:58.382550Z","iopub.status.idle":"2025-06-18T08:45:59.192589Z","shell.execute_reply.started":"2025-06-18T08:45:58.382527Z","shell.execute_reply":"2025-06-18T08:45:59.191915Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧪 **2.2 Data Validation – Kiểm tra chất lượng & chuẩn đầu vào**","metadata":{}},{"cell_type":"code","source":"df_full_4_6.isnull().sum().sort_values(ascending=False)[:5]","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:59.193776Z","iopub.execute_input":"2025-06-18T08:45:59.194143Z","iopub.status.idle":"2025-06-18T08:45:59.631888Z","shell.execute_reply.started":"2025-06-18T08:45:59.194112Z","shell.execute_reply":"2025-06-18T08:45:59.630791Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6.duplicated().sum()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:45:59.632825Z","iopub.execute_input":"2025-06-18T08:45:59.633098Z","iopub.status.idle":"2025-06-18T08:46:00.893612Z","shell.execute_reply.started":"2025-06-18T08:45:59.633076Z","shell.execute_reply":"2025-06-18T08:46:00.892656Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_4_6.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_4_6)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:00.894549Z","iopub.execute_input":"2025-06-18T08:46:00.894799Z","iopub.status.idle":"2025-06-18T08:46:01.328494Z","shell.execute_reply.started":"2025-06-18T08:46:00.894779Z","shell.execute_reply":"2025-06-18T08:46:01.327548Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_full_4_6)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:01.329477Z","iopub.execute_input":"2025-06-18T08:46:01.329787Z","iopub.status.idle":"2025-06-18T08:46:02.998227Z","shell.execute_reply.started":"2025-06-18T08:46:01.329757Z","shell.execute_reply":"2025-06-18T08:46:02.997244Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dữ liệu trước khi đưa vào mô hình cần được kiểm định cẩn thận để đảm bảo chất lượng và độ tin cậy. Dưới đây là những kiểm định đã được thực hiện trên tập dữ liệu hiện tại:\n\n| **Kiểm định**               | **Phát hiện & Xử lý**                                                                                                                                   |\n|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **Schema Validation**       | ✅ Đã rà soát và phát hiện một số cột có kiểu dữ liệu chưa phù hợp: `Order_date`, `VSD` hiện là *object*, đã chuyển về *datetime*. Một số cột như `CLASSIFY_CD`, `GLOBAL_NO`, `CUST_CD` đã chuẩn hóa về kiểu *object*. |\n| **Null / Missing Constraints** | ⚠️ Tỷ lệ thiếu giá trị tổng thể là **2.25%**. Hai cột có missing gần như toàn bộ là `QTUF_RCV_NO` (**99.85%**) và `SOUF_RCV_NO` (**96.28%**) → cần cân nhắc loại bỏ hoặc xử lý đặc biệt. |\n| **Unique Constraint**       | 🔁 Không có giá trị trùng lặp (**0%**) .                                                  |\n| **Range Checks**            | 📏 Đang trong quá trình kiểm tra các cột số liệu để đảm bảo nằm trong khoảng hợp lý. Ví dụ: các cột về thời gian, số lượng, mã vùng cần được kiểm soát kỹ.                                   |\n| **Constant/Skewed Feature Detection** | 📉 Một số cột như `label` chỉ có duy nhất một giá trị hoặc phân bố cực lệch → khi thực hiện huấn luyện model cần các kỹ thuật để cân bằng dữ liệu.                                                      |\n| **Special Missing Logic**   | 🧠 Một số cột có logic riêng cho giá trị thiếu: `OTHER_AREA_SHIP_DIV` với khoảng trắng là \"giao trong vùng sở hữu\", giá trị “1” là \"giao vùng ngoài\" → đã xử lý đúng theo business rule. |\n| **Type Inference**          | 🤖 Kết hợp kiểm tra thủ công và inference tự động để phát hiện các kiểu dữ liệu chưa chính xác hoặc bị ingest sai.                                                                    |\n| **Regex Pattern Checks**    | ⌨️ Đã rà soát các cột định danh như mã khách hàng, mã phân loại,... để đảm bảo định dạng đúng chuẩn.                                                                         |\n","metadata":{}},{"cell_type":"markdown","source":"## 👁️👁️ 2.3 EDA - Phân tích thăm dò","metadata":{}},{"cell_type":"markdown","source":"### 2.3.1 ✅ Tổng quan","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra mối quan hệ giữa `label` và `order date`","metadata":{}},{"cell_type":"code","source":"# Chuyển 'Order date' làm index\ndf_plot = df_full_4_6.set_index('Order date')\n\n# Resample theo ngày (có thể thay bằng 'W' cho tuần hoặc 'M' cho tháng nếu muốn smooth hơn)\nlabel_1_by_day = df_plot[df_plot['label'] == 1].resample('D').size()\n\nplt.figure(figsize=(14, 6))\nplt.plot(label_1_by_day.index, label_1_by_day.values, color='red', label='Số lượng label = 1')\n\nplt.title('Số lần xuất hiện label = 1 theo thời gian')\nplt.xlabel('Thời gian')\nplt.ylabel('Tần suất')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:02.999071Z","iopub.execute_input":"2025-06-18T08:46:02.999314Z","iopub.status.idle":"2025-06-18T08:46:04.017555Z","shell.execute_reply.started":"2025-06-18T08:46:02.999296Z","shell.execute_reply":"2025-06-18T08:46:04.016574Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như tần xuất đơn hàng bị delay chủ yếu vào giữa tháng, tiêu biểu là khoảng thời gian giữa tháng 4 và giữa tháng 5. Tuy nhiên tháng 6 xu hướng này không được thiết lập. Đồng thời thứ 6 không có dữ liệu được ghi nhận.","metadata":{}},{"cell_type":"code","source":"# Đảm bảo cột 'Order date' là datetime\ndf_full_4_6['Order date'] = pd.to_datetime(df_full_4_6['Order date'])\n\ndf_plot = df_full_4_6.set_index('Order date')\n\n# Resample theo ngày (có thể đổi 'D' -> 'W' hoặc 'M' nếu cần mượt hơn)\nlabel_1_by_day = df_plot[df_plot['label'] == 1].resample('D').size()\nlabel_0_by_day = df_plot[df_plot['label'] == 0].resample('D').size()\n\nplt.figure(figsize=(14, 6))\nplt.plot(label_1_by_day.index, label_1_by_day.values, color='red', label='Label = 1')\nplt.plot(label_0_by_day.index, label_0_by_day.values, color='blue', label='Label = 0')\n\nplt.title('Tần suất label = 1 (đỏ) và label = 0 (xanh) theo thời gian')\nplt.xlabel('Thời gian')\nplt.ylabel('Số lượng')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:04.018661Z","iopub.execute_input":"2025-06-18T08:46:04.018966Z","iopub.status.idle":"2025-06-18T08:46:06.874153Z","shell.execute_reply.started":"2025-06-18T08:46:04.018943Z","shell.execute_reply":"2025-06-18T08:46:06.873002Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Phân tích định danh đơn hàng","metadata":{}},{"cell_type":"code","source":"# Lấy top 50 GLOBAL_NO xuất hiện nhiều nhất\ntop_50_global_no = df_full_4_6['GLOBAL_NO'].value_counts().head(50)\n\nglobal_no_labels = df_full_4_6.groupby('GLOBAL_NO')['label'].min()\n\ncolors = ['red' if global_no_labels[global_no] == 1 else 'blue' for global_no in top_50_global_no.index]\n\nplt.figure(figsize=(18, 8))\nbars = plt.bar(top_50_global_no.index.astype(str), top_50_global_no.values, color=colors)\n\n# Gắn nhãn số lượng lên từng cột\nfor i, bar in enumerate(bars):\n    height = bar.get_height()\n    plt.text(bar.get_x() + bar.get_width()/2, height + 0.5,\n             str(int(height)), ha='center', va='bottom', fontsize=8, fontweight='bold')\n\nplt.title('Top 50 GLOBAL_NO - Phân màu theo Label (1: đỏ, 0: xanh)', fontsize=14)\nplt.xlabel('GLOBAL_NO')\nplt.ylabel('Số lần xuất hiện')\nplt.xticks(rotation=75, ha='right', fontsize=8)\nplt.grid(axis='y')\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:06.875093Z","iopub.execute_input":"2025-06-18T08:46:06.875392Z","iopub.status.idle":"2025-06-18T08:46:08.767334Z","shell.execute_reply.started":"2025-06-18T08:46:06.875371Z","shell.execute_reply":"2025-06-18T08:46:08.766520Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy xu hướng `Global_no` là khóa chính của dữ liệu","metadata":{}},{"cell_type":"markdown","source":"#### Top 10 SUPPLIER_CD có nhiều đơn nhất","metadata":{}},{"cell_type":"code","source":"grouped = df_full_4_6.groupby(['SUPPLIER_CD', 'label']).size().unstack(fill_value=0)\n\n\ntop10_suppliers = grouped.sum(axis=1).sort_values(ascending=False).head(20)\n\ntop_grouped = grouped.loc[top10_suppliers.index]\nplt.figure(figsize=(10, 6))\n\nplt.bar(top_grouped.index.astype(str), top_grouped[0], label='Label 0', color='lightcoral')\nplt.bar(top_grouped.index.astype(str), top_grouped[1], bottom=top_grouped[0], label='Label 1', color='cornflowerblue')\n\nplt.xlabel('SUPPLIER_CD')\nplt.ylabel('Số đơn hàng')\nplt.title('Stacked Bar Chart: Top 20 Supplier theo Label')\nplt.xticks(rotation=45)\nplt.legend(title='Label')\nplt.tight_layout()\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:08.768251Z","iopub.execute_input":"2025-06-18T08:46:08.768605Z","iopub.status.idle":"2025-06-18T08:46:09.296946Z","shell.execute_reply.started":"2025-06-18T08:46:08.768575Z","shell.execute_reply":"2025-06-18T08:46:09.295927Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy nhà cung cấp với mã code 3081 có tỉ lệ đơn hàng delay rất lớn so với các nhà cung cấp khác. Nhà cung cấp 3186 và 8484 cũng có số lượng đơn hàng bị delay có thể quan sát qua biểu đồ.","metadata":{}},{"cell_type":"markdown","source":"### 2.3.2 🚛 Vận chuyển","metadata":{}},{"cell_type":"markdown","source":"#### So sánh ACTUAL_SHIP_DAYS vs SPECIFY_SHIP_DAYS - Số ngày làm việc thực tế từ thời điểm đặt hàng đến ngày dự kiến giao (VSD) vs Số ngày vận chuyển được chỉ định bởi bộ phận front desk ","metadata":{}},{"cell_type":"code","source":"vc = df_full_4_6['SPECIFY_SHIP_DAYS'].value_counts()\ndf_ngang = vc.to_frame().T\ndf_ngang.reset_index(drop=True, inplace=True)\n\ndf_ngang","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:09.297977Z","iopub.execute_input":"2025-06-18T08:46:09.298293Z","iopub.status.idle":"2025-06-18T08:46:09.330703Z","shell.execute_reply.started":"2025-06-18T08:46:09.298261Z","shell.execute_reply":"2025-06-18T08:46:09.329840Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"vc = df_full_4_6['ACTUAL_SHIP_DAYS'].value_counts()\ndf_ngang = vc.to_frame().T\ndf_ngang.reset_index(drop=True, inplace=True)\n\ndf_ngang","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:09.331782Z","iopub.execute_input":"2025-06-18T08:46:09.332205Z","iopub.status.idle":"2025-06-18T08:46:09.361129Z","shell.execute_reply.started":"2025-06-18T08:46:09.332084Z","shell.execute_reply":"2025-06-18T08:46:09.360347Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"grouped = df_full_4_6.groupby(['ACTUAL_SHIP_DAYS', 'label']).size().unstack(fill_value=0)\ngrouped = grouped.sort_index().head(50)\n\nplt.figure(figsize=(14, 6))\n\nplt.bar(grouped.index.astype(str), grouped[0], label='Label 0', color='orchid')\nplt.bar(grouped.index.astype(str), grouped[1], bottom=grouped[0], label='Label 1', color='mediumslateblue')\n\nplt.xlabel('ACTUAL_SHIP_DAYS')\nplt.ylabel('Số lượng đơn hàng')\nplt.title('Stacked Bar Chart: Phân phối số ngày giao hàng thực tế theo Label')\nplt.xticks(rotation=45)\nplt.legend(title='Label')\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:09.362136Z","iopub.execute_input":"2025-06-18T08:46:09.362405Z","iopub.status.idle":"2025-06-18T08:46:09.977564Z","shell.execute_reply.started":"2025-06-18T08:46:09.362384Z","shell.execute_reply":"2025-06-18T08:46:09.976695Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dòng có label = delay chủ yếu rơi vào, những ngày ngắn của actual ship day. Có thể do là ở gần, sẵn hàng nhưng dự kiến giao trong ngày, nhưng vì lý do gì đó nên delay qua hôm sau.","metadata":{}},{"cell_type":"code","source":"grouped = df_full_4_6.groupby(['SPECIFY_SHIP_DAYS', 'label']).size().unstack(fill_value=0)\n\n# Bước 2: Giới hạn top 50 ngày đầu tiên (đảm bảo đã sort)\ngrouped = grouped.sort_index().head(50)\n\n# Bước 3: Vẽ stacked bar chart\nplt.figure(figsize=(14, 6))\n\nplt.bar(grouped.index.astype(str), grouped[0], label='Label 0', color='orchid')\nplt.bar(grouped.index.astype(str), grouped[1], bottom=grouped[0], label='Label 1', color='mediumslateblue')\n\nplt.xlabel('SPECIFY_SHIP_DAYS')\nplt.ylabel('Số lượng đơn hàng')\nplt.title('Stacked Bar Chart: Phân phối số ngày Số ngày vận chuyển được chỉ định bởi bộ phận front desk theo Label')\nplt.xticks(rotation=45)\nplt.legend(title='Label')\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:09.978802Z","iopub.execute_input":"2025-06-18T08:46:09.979154Z","iopub.status.idle":"2025-06-18T08:46:10.588151Z","shell.execute_reply.started":"2025-06-18T08:46:09.979125Z","shell.execute_reply":"2025-06-18T08:46:10.587245Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Có vẻ như cột này có vấn đề rất lớn khi hầu hết dữ liệu đều là -1. Một số lý do có thể lý giải như -1 là giá trị `Số ngày vận chuyển được chỉ định bởi bộ phận front desk` còn nếu có giá trị khác -1 thì là `trường hợp chỉ set khi có yêu cầu đặc biệt`","metadata":{}},{"cell_type":"markdown","source":"#### So sánh DIRECT SHIP FLG, DELI_DIV, Ship Mode","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Các cột cần vẽ\ncols = ['DIRECT SHIP FLG', 'DELI_DIV', 'Ship Mode']\nlabel_col = 'label'\n\n# Tạo Figure với 3 subplot nằm trên 1 dòng\nfig, axes = plt.subplots(1, 3, figsize=(18, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm và đếm theo giá trị + label\n    grouped = df_full_4_6.groupby([col, label_col]).size().unstack(fill_value=0)\n\n    # Đảm bảo có cả label 0 và 1\n    if 0 not in grouped.columns:\n        grouped[0] = 0\n    if 1 not in grouped.columns:\n        grouped[1] = 0\n\n    grouped = grouped[[0, 1]]  # Giữ thứ tự\n\n    # Tính số lượng giá trị unique\n    n_unique = df_full_4_6[col].nunique()\n\n    # Vẽ stacked bar chart\n    ax.bar(grouped.index.astype(str), grouped[0], label='Label 0', color='lightcoral')\n    ax.bar(grouped.index.astype(str), grouped[1], bottom=grouped[0], label='Label 1', color='steelblue')\n    \n    # Gắn tiêu đề có số lượng unique\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xticklabels(grouped.index.astype(str), rotation=45, ha='right')\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\n# Gán nhãn chung\nfig.suptitle('Phân phối Label theo DIRECT_SHIP_FLG, DELI_DIV và Ship Mode', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:10.589184Z","iopub.execute_input":"2025-06-18T08:46:10.589444Z","iopub.status.idle":"2025-06-18T08:46:11.388792Z","shell.execute_reply.started":"2025-06-18T08:46:10.589424Z","shell.execute_reply":"2025-06-18T08:46:11.387892Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối theo label của `DIRECT SHIP FLG` có vẻ khá cân bằng, điều này khá tương tự với trường hợp của `Ship mode`. Trái lại trường `DELI_DIV - Mã phương thức vận chuyển đơn hàng` cho thấy delay lớn ở phương thức có mã vận chuyển là `0`","metadata":{}},{"cell_type":"markdown","source":"### 2.3.3 📦 Sản phẩm","metadata":{}},{"cell_type":"markdown","source":"### Kiểm tra tỷ lệ trễ của hàng nặng (HEAVY_FLG), hàng nguy hiểm (HAZARD_FLG), hàng đắt (EXPENSIVE_FLG), loại tồn kho (Stock class)","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Các cột flag cần vẽ + 1 cột rời rạc thêm\ncols = ['HEAVY_FLG', 'HAZARD_FLG', 'EXPENSIVE_FLG', 'Stock class']\nlabel_col = 'label'\n\n# Tạo Figure với 4 subplot nằm trên 1 dòng\nfig, axes = plt.subplots(1, 4, figsize=(24, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm và đếm theo giá trị + label\n    grouped = df_full_4_6.groupby([col, label_col]).size().unstack(fill_value=0)\n\n    # Đảm bảo có cả label 0 và 1\n    for lbl in [0, 1]:\n        if lbl not in grouped.columns:\n            grouped[lbl] = 0\n\n    grouped = grouped[[0, 1]]  # Đảm bảo đúng thứ tự\n\n    # Tính số lượng giá trị unique\n    n_unique = df_full_4_6[col].nunique()\n\n    # Vẽ stacked bar chart\n    x = grouped.index.astype(str)\n    bar1 = ax.bar(x, grouped[0], label='Label 0', color='lightcoral')\n    bar2 = ax.bar(x, grouped[1], bottom=grouped[0], label='Label 1', color='steelblue')\n    \n    # Gắn số lượng lên đầu mỗi cột\n    for j in range(len(x)):\n        total = grouped[0].iloc[j] + grouped[1].iloc[j]\n        ax.text(j, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n    # Gắn tiêu đề có số lượng unique\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xticklabels(x, rotation=30, ha='center')\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\n# Gán nhãn chung\nfig.suptitle('Phân phối Label theo các cột Flag và Stock Class', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:11.389920Z","iopub.execute_input":"2025-06-18T08:46:11.390226Z","iopub.status.idle":"2025-06-18T08:46:12.163173Z","shell.execute_reply.started":"2025-06-18T08:46:11.390198Z","shell.execute_reply":"2025-06-18T08:46:12.162255Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `label` theo các cờ về các thuộc tính của mặt hành như nặng, nguy hiểm, đắt, loại tồn kho không phản ánh 1 xu hướng rõ ràng nào để đưa ra kết luận.","metadata":{}},{"cell_type":"markdown","source":"#### Khối lượng `WEIGHT PER PIECE - Khối lượng từng món hàng` ảnh hưởng ra sao?","metadata":{}},{"cell_type":"code","source":"top_weights = df_full_4_6['WEIGHT PER PIECE'].value_counts().head(30).index\n\n# B2: Lọc dữ liệu theo top_weights\ndf_top = df_full_4_6[df_full_4_6['WEIGHT PER PIECE'].isin(top_weights)]\n\n# B3: Nhóm theo weight và label\ngrouped = df_top.groupby(['WEIGHT PER PIECE', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo có cả label 0 và 1\nfor lbl in [0, 1]:\n    if lbl not in grouped.columns:\n        grouped[lbl] = 0\n\ngrouped = grouped[[0, 1]]  # Đúng thứ tự\n\n# B4: Vẽ stacked bar chart\nplt.figure(figsize=(12, 6))\nx = grouped.index.astype(str)\n\nbar1 = plt.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\nbar2 = plt.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n\n# Thêm số lượng tổng mỗi cột\nfor i in range(len(x)):\n    total = grouped[0].iloc[i] + grouped[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\nplt.title('Phân phối Label theo các trọng lượng phổ biến nhất')\nplt.xlabel('WEIGHT PER PIECE')\nplt.ylabel('Số lượng đơn hàng')\nplt.xticks(rotation=45)\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:12.164164Z","iopub.execute_input":"2025-06-18T08:46:12.164430Z","iopub.status.idle":"2025-06-18T08:46:12.753408Z","shell.execute_reply.started":"2025-06-18T08:46:12.164409Z","shell.execute_reply":"2025-06-18T08:46:12.752368Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_4_6['WEIGHT_UNIT'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:12.754308Z","iopub.execute_input":"2025-06-18T08:46:12.754558Z","iopub.status.idle":"2025-06-18T08:46:12.784243Z","shell.execute_reply.started":"2025-06-18T08:46:12.754539Z","shell.execute_reply":"2025-06-18T08:46:12.783334Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `Khối lượng từng món hàng` theo label khá giống nhau. Ta biết thêm 1 thông tin là đa phần Khối lượng của từng món hàng chủ yếu có khối lượng nhỏ","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra phân phối của label với từng loại SHIP MODE","metadata":{}},{"cell_type":"code","source":"top_10_modes = df_full_4_6['Ship Mode'].value_counts().head(20).index\n\n# Lọc dữ liệu chỉ giữ lại các Ship Mode nằm trong top 10\nfiltered_df = df_full_4_6[df_full_4_6['Ship Mode'].isin(top_10_modes)]\n\n# Đếm theo Ship Mode và label\nshipmode_counts = filtered_df.groupby(['Ship Mode', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đúng thứ tự\nshipmode_counts = shipmode_counts.loc[top_10_modes]  # giữ đúng thứ tự top 10\nshipmode_counts = shipmode_counts[[0, 1]]  # label 0 trước, rồi label 1\n\n# Vẽ stacked bar chart\nplt.figure(figsize=(12, 6))\nx = shipmode_counts.index.astype(str)\nbar1 = plt.bar(x, shipmode_counts[0], label='Label 0', color='mediumseagreen')\nbar2 = plt.bar(x, shipmode_counts[1], bottom=shipmode_counts[0], label='Label 1', color='tomato')\n\n# Gắn số lượng lên mỗi cột\nfor i in range(len(x)):\n    total = shipmode_counts[0].iloc[i] + shipmode_counts[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Trang trí\nplt.title('🚛 Phân phối Ship Mode theo Label (Top 10 phổ biến nhất)', fontsize=14)\nplt.xlabel('Ship Mode')\nplt.ylabel('Số lượng đơn hàng')\nplt.xticks(rotation=45)\nplt.grid(axis='y', linestyle='--', alpha=0.5)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:12.785195Z","iopub.execute_input":"2025-06-18T08:46:12.785444Z","iopub.status.idle":"2025-06-18T08:46:13.625514Z","shell.execute_reply.started":"2025-06-18T08:46:12.785418Z","shell.execute_reply":"2025-06-18T08:46:13.624636Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như không có mối quan hệ nào giữa `label` và `SHIP MODE`","metadata":{}},{"cell_type":"markdown","source":"### 2.3.4 📅 Thời gian","metadata":{}},{"cell_type":"markdown","source":"#### Phân tích theo SO_DAY_OF_WEEK và SO_DAY_OF_MONTH","metadata":{}},{"cell_type":"markdown","source":"🔔 **Lưu ý quan trọng về trường `SO_DAY_OF_WEEK`:**\nTrong dữ liệu, cột `SO_DAY_OF_WEEK` chứa các giá trị rời rạc đại diện cho các ngày trong tuần, cụ thể như sau:\n\n* `1` → **Chủ nhật**\n* `2` → **Thứ Bảy**\n* `3` → **Thứ Hai**\n* `4` → **Thứ Ba**\n* `5` → **Thứ Tư**\n* `6` → **Thứ Năm**\n\n❗️**Không có giá trị nào tương ứng với Thứ Sáu** trong dữ liệu này. Đây là một điểm cần đặc biệt lưu ý khi xử lý hoặc trực quan hóa theo ngày trong tuần.\n","metadata":{}},{"cell_type":"code","source":"cols = ['SO_DAY_OF_WEEK', 'SO_DAY_OF_MONTH']\nfig, axes = plt.subplots(1, 2, figsize=(18, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm dữ liệu theo giá trị + label\n    grouped = df_full_4_6.groupby([col, 'label']).size().unstack(fill_value=0)\n\n    # Đảm bảo cả label 0 và 1 đều có\n    for lbl in [0, 1]:\n        if lbl not in grouped.columns:\n            grouped[lbl] = 0\n\n    grouped = grouped[[0, 1]]  # Đúng thứ tự\n    \n    # Tính số lượng giá trị unique\n    n_unique = df_full_4_6[col].nunique()\n    \n    # Vẽ stacked bar chart\n    x = grouped.index.astype(str)\n    bar1 = ax.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\n    bar2 = ax.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n    \n    # In số lượng trên mỗi cột\n    for j in range(len(x)):\n        total = grouped[0].iloc[j] + grouped[1].iloc[j]\n        ax.text(j, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n    # Gắn tiêu đề\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xlabel(col)\n    ax.set_xticks(range(len(x)))\n    ax.set_xticklabels(x, rotation=0)\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\nfig.suptitle('Phân phối Đơn hàng theo Ngày trong tuần & Ngày trong tháng', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị ngày', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:13.626444Z","iopub.execute_input":"2025-06-18T08:46:13.626695Z","iopub.status.idle":"2025-06-18T08:46:14.402753Z","shell.execute_reply.started":"2025-06-18T08:46:13.626675Z","shell.execute_reply":"2025-06-18T08:46:14.401843Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `delay` và `not delay` khá cân bằng giữa các ngày trong tuần và các ngày trong tháng.","metadata":{}},{"cell_type":"markdown","source":"####  Phân tích mối quan hệ giữa giờ đặt hàng (SO_TIME) và label (trễ/không trễ)","metadata":{}},{"cell_type":"code","source":"# Bước 1: Tính SO_HOUR tạm thời, không thêm vào df_full_4_6\ntemp_df = df_full_4_6.copy()\ntemp_df['SO_HOUR'] = temp_df['SO_TIME'].apply(lambda x: x.hour if pd.notna(x) else None)\n\n# Bước 2: Group theo giờ và label\nhourly_counts = temp_df.groupby(['SO_HOUR', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đủ 24 giờ (kể cả giờ không có đơn)\nall_hours = pd.Series(range(0, 24), name='SO_HOUR')\nhourly_counts = hourly_counts.reindex(all_hours, fill_value=0)\n\n# Bước 3: Vẽ stacked bar chart\nplt.figure(figsize=(14, 6))\nbars_0 = plt.bar(hourly_counts.index, hourly_counts[0], label='Không trễ (label=0)', color='mediumseagreen')\nbars_1 = plt.bar(hourly_counts.index, hourly_counts[1], bottom=hourly_counts[0], label='Trễ (label=1)', color='tomato')\n\n# Gắn số lượng tổng lên đỉnh\nfor i in hourly_counts.index:\n    total = hourly_counts.loc[i, 0] + hourly_counts.loc[i, 1]\n    if total > 0:\n        plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Cài đặt biểu đồ\nplt.xlabel('Giờ đặt hàng (0–23h)')\nplt.ylabel('Số đơn hàng')\nplt.title('Phân phối đơn hàng theo giờ đặt (SO_TIME) và Label (Trễ / Không trễ)')\nplt.xticks(range(24))\nplt.legend()\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:14.403718Z","iopub.execute_input":"2025-06-18T08:46:14.403998Z","iopub.status.idle":"2025-06-18T08:46:15.664585Z","shell.execute_reply.started":"2025-06-18T08:46:14.403970Z","shell.execute_reply":"2025-06-18T08:46:15.663499Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Mối quan hệ giữa giờ đặt hàng (SO_TIME) và label (trễ/không trễ) khá bình thường, phân bố đều","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra số lượng các ngày lễ/nghỉ (Consider count hodiday Saturday) có ảnh hưởng gì không?","metadata":{}},{"cell_type":"code","source":"col = 'Consider count hodiday Saturday'\n\n# Gom nhóm theo số ngày đặc biệt + label\ngrouped = df_full_4_6.groupby([col, 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đủ 2 nhãn label\nfor lbl in [0, 1]:\n    if lbl not in grouped.columns:\n        grouped[lbl] = 0\n\ngrouped = grouped[[0, 1]]  # đúng thứ tự\n\n# Lấy các giá trị phổ biến đầu tiên (tránh quá dài)\ngrouped = grouped.sort_index().head(10)\n\n# Bắt đầu vẽ\nplt.figure(figsize=(10, 6))\n\nx = grouped.index.astype(str)\nbar1 = plt.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\nbar2 = plt.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n\n# Gắn số lượng lên đầu cột\nfor i in range(len(x)):\n    total = grouped[0].iloc[i] + grouped[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Thêm info\nplt.title(f'Phân phối đơn hàng theo Số ngày lễ và ngày nghỉ cuối tuần từ ngày đặt hàng đến ngày giao hàng\\nBiến: \"{col}\" (Unique: {df_full_4_6[col].nunique()})', fontsize=14)\nplt.xlabel('Số ngày lễ + Thứ 7')\nplt.ylabel('Số lượng đơn hàng')\nplt.grid(axis='y', linestyle='--', alpha=0.5)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:15.666089Z","iopub.execute_input":"2025-06-18T08:46:15.666365Z","iopub.status.idle":"2025-06-18T08:46:15.952572Z","shell.execute_reply.started":"2025-06-18T08:46:15.666345Z","shell.execute_reply":"2025-06-18T08:46:15.951625Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như `Số ngày lễ và ngày nghỉ cuối tuần từ ngày đặt hàng đến ngày giao hàng` không có mối quan hệ rõ rệt tới việc đơn hàng có bị delay hay không","metadata":{}},{"cell_type":"markdown","source":"### 🥹 2.3.5 Khác","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra các trường có ít giá trị unique - các trường chỉ toàn là 1 giá trị","metadata":{}},{"cell_type":"code","source":"def get_highly_dominant_columns(df, threshold=0.99):\n    \"\"\"\n    Trả về danh sách các cột mà 1 giá trị chiếm ≥ threshold (default 0.99 = 99%)\n    \"\"\"\n    dominant_cols = []\n\n    for col in df.columns:\n        value_counts = df[col].value_counts(normalize=True, dropna=False)\n        if not value_counts.empty:\n            top_freq = value_counts.values[0]\n            if top_freq >= threshold:\n                dominant_val = value_counts.idxmax()\n                dominant_cols.append((col, top_freq, dominant_val))\n\n    return dominant_cols","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:15.953561Z","iopub.execute_input":"2025-06-18T08:46:15.953912Z","iopub.status.idle":"2025-06-18T08:46:15.960279Z","shell.execute_reply.started":"2025-06-18T08:46:15.953884Z","shell.execute_reply":"2025-06-18T08:46:15.959214Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dominant_columns_info = get_highly_dominant_columns(df_full_4_6, threshold=0.99)\n\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:15.961346Z","iopub.execute_input":"2025-06-18T08:46:15.961771Z","iopub.status.idle":"2025-06-18T08:46:17.374188Z","shell.execute_reply.started":"2025-06-18T08:46:15.961738Z","shell.execute_reply":"2025-06-18T08:46:17.373290Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tìm **những cột mà trong đó có một giá trị chiếm >99%**, tức là mất cân bằng dữ liệu cực kỳ cao → có thể không hữu ích cho mô hình hóa hoặc gây nhiễu/overfit, nên ở bước sau ta sẽ xóa.","metadata":{}},{"cell_type":"markdown","source":"# 3. DATA CLEANING & REPAIRING & SLIVER Layer Data","metadata":{}},{"cell_type":"markdown","source":"## 🫳**3.1 Drop meaningless column**","metadata":{}},{"cell_type":"code","source":"# Gọi hàm\ndominant_columns_info = get_highly_dominant_columns(df_full_4_6, threshold=0.99)\n\n# In danh sách trước khi xoá\nprint(\"📌 Các cột sẽ bị drop (1 giá trị chiếm ≥99%):\")\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")\n\n# Thực hiện drop\ncolumns_to_drop = [col for col, _, _ in dominant_columns_info]\ndf_full_4_6 = df_full_4_6.drop(columns=columns_to_drop)\n\nprint(f\"\\n✅ Đã xoá {len(columns_to_drop)} cột. DataFrame giờ còn {df_full_4_6.shape[1]} cột.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:17.375084Z","iopub.execute_input":"2025-06-18T08:46:17.375327Z","iopub.status.idle":"2025-06-18T08:46:18.947757Z","shell.execute_reply.started":"2025-06-18T08:46:17.375309Z","shell.execute_reply":"2025-06-18T08:46:18.946917Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧽 **3.2 Detecting And Handling Missing Values**","metadata":{}},{"cell_type":"code","source":"missing_info = df_full_4_6.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_4_6)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:18.948784Z","iopub.execute_input":"2025-06-18T08:46:18.949102Z","iopub.status.idle":"2025-06-18T08:46:19.278602Z","shell.execute_reply.started":"2025-06-18T08:46:18.949080Z","shell.execute_reply":"2025-06-18T08:46:19.277897Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SOUF_RCV_NO`","metadata":{"id":"KiF3f1IPP3xb"}},{"cell_type":"markdown","source":"Ta nhận thấy trường `SOUF_RCV_NO - Số tiếp tân không hợp lệ tại thời điểm đặt hàng` bị missing nhiều nhất - 96.28%, đây là dấu hiệu `MNAR - Missing Not At Random - Missing do nội tại của cột đó`. Việc điền missing value bằng các kỹ thuật là điều không thể do logic không rõ ràng và thiếu phần lớn dữ liệu, nên ta sẽ bỏ cột trên.","metadata":{"id":"oA9pr2qer5jo"}},{"cell_type":"code","source":"df_full_4_6.drop(columns=['SOUF_RCV_NO'], inplace=True)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:19.279483Z","iopub.execute_input":"2025-06-18T08:46:19.279782Z","iopub.status.idle":"2025-06-18T08:46:19.388115Z","shell.execute_reply.started":"2025-06-18T08:46:19.279760Z","shell.execute_reply":"2025-06-18T08:46:19.387234Z"},"id":"uruc3fspraOI","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_4_6.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_4_6)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:19.388967Z","iopub.execute_input":"2025-06-18T08:46:19.389230Z","iopub.status.idle":"2025-06-18T08:46:19.700772Z","shell.execute_reply.started":"2025-06-18T08:46:19.389207Z","shell.execute_reply":"2025-06-18T08:46:19.699462Z"},"id":"FH8DlTcUugHy","outputId":"3382c029-6d5b-4026-886e-011c668aed0d","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SHIP DECISION NO` và `SHIP MODE`","metadata":{"id":"vBg4clWgQCVC"}},{"cell_type":"code","source":"# Chuẩn bị dữ liệu biểu đồ\ncols_to_check = ['SHIP DECISION NO', 'Ship Mode']\nmissing_stats = []\n\nfor label_value in [0, 1]:\n    subset = df_full_4_6[df_full_4_6['label'] == label_value]\n    total_rows = len(subset)\n\n    for col in cols_to_check:\n        missing_count = subset[col].isnull().sum()\n        missing_percent = (missing_count / total_rows) * 100\n        missing_stats.append({\n            'Label': f'label = {label_value}',\n            'Column': col,\n            'Missing %': missing_percent\n        })\n\n# Tạo DataFrame từ kết quả\nmissing_df = pd.DataFrame(missing_stats)\n\n# Vẽ biểu đồ\nplt.figure(figsize=(8, 5))\nfor col in cols_to_check:\n    subset_df = missing_df[missing_df['Column'] == col]\n    plt.bar(subset_df['Label'], subset_df['Missing %'], label=col)\n\nplt.ylabel('Missing Value (%)')\nplt.title('Tỉ lệ Missing Value theo label')\nplt.legend(title='Column')\nplt.ylim(0, max(missing_df['Missing %']) + 5)\nplt.grid(axis='y', linestyle='--', alpha=0.7)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:19.702550Z","iopub.execute_input":"2025-06-18T08:46:19.703009Z","iopub.status.idle":"2025-06-18T08:46:20.087022Z","shell.execute_reply.started":"2025-06-18T08:46:19.702976Z","shell.execute_reply":"2025-06-18T08:46:20.086098Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_delay_4_6['SHIP DECISION NO'].isnull().sum()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.087950Z","iopub.execute_input":"2025-06-18T08:46:20.088376Z","iopub.status.idle":"2025-06-18T08:46:20.095216Z","shell.execute_reply.started":"2025-06-18T08:46:20.088346Z","shell.execute_reply":"2025-06-18T08:46:20.094305Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Có 2 điều bất ngờ:\n- khi giá trị của cột `SHIP DECISION NO` chỉ bị missing chủ yếu ở đơn hàng đó không bị delay. Trái lại nếu đơn hàng bị delay thì tỉ lệ của `SHIP DECISION NO` bị missing là rất ít. Điều này gợi ý rằng nếu cột `SHIP DECISION NO` bị missing ở đơn hàng nào thì nhiều khả năng đơn hàng đó chắc chắn không bị giao trễ.\n- Tỉ lệ giá trị của `SHIP MODE` bị missing nhiều hơn khi `label = 1` gấp 3 lần khi `label = 0`. Điều này gợi ý rằng nếu cột `SHIP MODE` bị missing thì có khả năng tại đó đơn hàng bị giao trễ.","metadata":{}},{"cell_type":"code","source":"df_full_4_6['SHIP DECISION NO'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.096163Z","iopub.execute_input":"2025-06-18T08:46:20.096495Z","iopub.status.idle":"2025-06-18T08:46:20.357760Z","shell.execute_reply.started":"2025-06-18T08:46:20.096468Z","shell.execute_reply":"2025-06-18T08:46:20.356612Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Vì tỉ lệ missing values của `SHIP DECISION NO` và `Ship Mode` trong dataset tương đối ít \n\n- Đối với `SHIP DECISION NO`, vì cột này là khóa ngoại (mã quyết định vận chuyển - với 249902 giá trị unique) nên không thể điền bằng logic hay thuật toán, nên việc điền missing values chủ quan chắc chắn sẽ gây vi phạm ràng buộc tham chiếu. Do ở trên ta đã phân tích và đưa ra giả thuyết rằng: 'nếu cột `SHIP DECISION NO` bị missing ở đơn hàng nào thì nhiều khả năng đơn hàng đó chắc chắn không bị giao trễ.' Nên ta sẽ thay các giá trị missing bằng giá trị -1, để bước model có thể học được mối quan hệ.\n\n- Đối với `Ship Mode`. Nếu chỉ loại bỏ các record chứa missing values có thể dẫn đến sai lầm vì có thể có một logic ẩn chứa bên trong, (đặc biệt là đơn hàng có thể hay bị delay bởi `Ship Mode`) do đó tương tự, ta sẽ điền giá trị `-1`, để bước model có thể học được mối quan hệ.","metadata":{"id":"xIs55kK0O0kt"}},{"cell_type":"code","source":"# Thay giá trị missing bằng -1 cho 2 cột được phân tích\ndf_full_4_6['SHIP DECISION NO'] = df_full_4_6['SHIP DECISION NO'].fillna(-1)\ndf_full_4_6['Ship Mode'] = df_full_4_6['Ship Mode'].fillna(-1)\n\n# In thông báo xác nhận\nprint(\"✅ Đã thay thế missing values trong 'SHIP DECISION NO' và 'Ship Mode' bằng -1.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.358759Z","iopub.execute_input":"2025-06-18T08:46:20.359040Z","iopub.status.idle":"2025-06-18T08:46:20.436562Z","shell.execute_reply.started":"2025-06-18T08:46:20.359006Z","shell.execute_reply":"2025-06-18T08:46:20.435605Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_4_6.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_4_6)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.437618Z","iopub.execute_input":"2025-06-18T08:46:20.437956Z","iopub.status.idle":"2025-06-18T08:46:20.731426Z","shell.execute_reply.started":"2025-06-18T08:46:20.437927Z","shell.execute_reply":"2025-06-18T08:46:20.730563Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý SUPPLIER_DIV","metadata":{}},{"cell_type":"code","source":"# Missing quá ít nên ta đơn giản là drop các dòng bị missing.\n# Drop các dòng bị thiếu ở SUPPLIER_DIV\ndf_full_4_6 = df_full_4_6.dropna(subset=['SUPPLIER_DIV'])","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.732444Z","iopub.execute_input":"2025-06-18T08:46:20.732713Z","iopub.status.idle":"2025-06-18T08:46:20.874432Z","shell.execute_reply.started":"2025-06-18T08:46:20.732692Z","shell.execute_reply":"2025-06-18T08:46:20.873529Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_4_6.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_4_6)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:20.875316Z","iopub.execute_input":"2025-06-18T08:46:20.875560Z","iopub.status.idle":"2025-06-18T08:46:21.173058Z","shell.execute_reply.started":"2025-06-18T08:46:20.875540Z","shell.execute_reply":"2025-06-18T08:46:21.172239Z"},"id":"EOmhgm-ePziN","outputId":"ae1acd0c-5267-47d3-ef17-57235421f815","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Đã xong missing value 🥹🥹","metadata":{}},{"cell_type":"markdown","source":"## 🔁 **3.3 Xử lý duplicate**","metadata":{}},{"cell_type":"markdown","source":"Không có trùng lặp (**0%**)\n","metadata":{}},{"cell_type":"code","source":"print_data_overview(df_full_4_6)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:21.173935Z","iopub.execute_input":"2025-06-18T08:46:21.174179Z","iopub.status.idle":"2025-06-18T08:46:22.335206Z","shell.execute_reply.started":"2025-06-18T08:46:21.174158Z","shell.execute_reply":"2025-06-18T08:46:22.334142Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##  🛠️ **3.4 Data Type Checking**","metadata":{"id":"ClAlO3lmlUde"}},{"cell_type":"code","source":"# Liệt kê tất cả các dtype của các cột trong dataframe\nprint(df_full_4_6.dtypes)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:22.336352Z","iopub.execute_input":"2025-06-18T08:46:22.336660Z","iopub.status.idle":"2025-06-18T08:46:22.343893Z","shell.execute_reply.started":"2025-06-18T08:46:22.336632Z","shell.execute_reply":"2025-06-18T08:46:22.342619Z"},"id":"vgUr59Iqe3Mk","outputId":"87665609-b868-42b6-da39-05737c234301","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🥈 3.3 SLIVER DATA","metadata":{}},{"cell_type":"markdown","source":"### Tạo file `sliver data` để lưu dữ liệu đã qua xử lý làm sạch","metadata":{}},{"cell_type":"code","source":"for col in df_full_4_6.select_dtypes(include='object').columns:\n    df_full_4_6[col] = df_full_4_6[col].astype(str)\n\ndf_full_4_6.to_parquet('/kaggle/working/Data for practice/df_full_4_6_sliver.parquet', index=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:22.344990Z","iopub.execute_input":"2025-06-18T08:46:22.345252Z","iopub.status.idle":"2025-06-18T08:46:24.915532Z","shell.execute_reply.started":"2025-06-18T08:46:22.345226Z","shell.execute_reply":"2025-06-18T08:46:24.914606Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Processing 7 - 9","metadata":{}},{"cell_type":"markdown","source":"Dữ liệu đã cho gồm có 2 bảng định dạng CSV và EXCEL\n\n- File `delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv` và `not_delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv` lưu các thông tin về các đơn hàng bị delay và KHÔNG bị delay từ tháng 7 -> 9. Sau khi nối lại với nhau gọi là df_full_7_9.\n\n- File `Sample codebook of Delay Prediction task.xlsx` lưu thông tin ý nghĩa các trường trong dataset. Sau đây gọi là codebook.\n\nTuy nhiên có 1 số điểm chưa được đồng bộ giữa dataset và codebook (codebook chưa mô tả đủ các trường), dễ gây hiểu lầm trong quá trình làm việc.","metadata":{}},{"cell_type":"markdown","source":"# 1. **Ingestion & Metadata Logging & BRONZE Layer Data**","metadata":{}},{"cell_type":"markdown","source":"## **1.1 Data Ingestion**","metadata":{}},{"cell_type":"code","source":"# Load data\nbase_path = r\"/kaggle/input/ds-108-p-21-assigment-06\"\n\n# Đọc từng file CSV vào DataFrame\ndf_delay_7_9 = pd.read_csv(f\"{base_path}/delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv\", encoding='utf-8')\ndf_not_delay_7_9 = pd.read_csv(f\"{base_path}/not_delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv\", encoding='utf-8')\n\nprint(df_delay_7_9.shape)\nprint(df_not_delay_7_9.shape)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:24.916826Z","iopub.execute_input":"2025-06-18T08:46:24.917199Z","iopub.status.idle":"2025-06-18T08:46:32.434516Z","shell.execute_reply.started":"2025-06-18T08:46:24.917168Z","shell.execute_reply":"2025-06-18T08:46:32.433647Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Nối delay và not delay thành data hoàn chỉnh\ndf_full_7_9 = pd.concat([df_delay_7_9, df_not_delay_7_9], ignore_index=True)\nprint(df_full_7_9.shape)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.435530Z","iopub.execute_input":"2025-06-18T08:46:32.435758Z","iopub.status.idle":"2025-06-18T08:46:32.640685Z","shell.execute_reply.started":"2025-06-18T08:46:32.435739Z","shell.execute_reply":"2025-06-18T08:46:32.639926Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load codebook\ncodebook = pd.read_excel(\n    \"/kaggle/input/m-company-delay-prediction/Sample codebook of Delay Prediction task.xlsx\",\n    sheet_name=\"Columns Details\",\n    header=0,\n    index_col=0\n)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.641498Z","iopub.execute_input":"2025-06-18T08:46:32.641712Z","iopub.status.idle":"2025-06-18T08:46:32.671257Z","shell.execute_reply.started":"2025-06-18T08:46:32.641696Z","shell.execute_reply":"2025-06-18T08:46:32.670372Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"số thuộc tính được liệt kê trong codebook: {codebook.shape[0]}\")\nprint(f\"số thuộc tính của df_full_7_9: {df_full_7_9.shape[1]}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.672207Z","iopub.execute_input":"2025-06-18T08:46:32.672467Z","iopub.status.idle":"2025-06-18T08:46:32.677884Z","shell.execute_reply.started":"2025-06-18T08:46:32.672445Z","shell.execute_reply":"2025-06-18T08:46:32.676952Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"> Có thể suy luận rằng có đủ thuộc tính được định nghĩa trong codebook. Tuy nhiên nếu khẳng định vừa rồi sẽ sai nếu:\n- 1 số thuộc tính trong codebook không hề có trong df_full_7_9 => có 1 số thuộc tính không được định nghĩa\n- 1 số thuộc tính trong codebook có thể định nghĩa cho nhiều trường trong df_full_7_9 => có ít hơn thuộc tính không được định nghĩa\n\n**Phần `c. Kiểm soát schema đầu vào (tên cột, kiểu dữ liệu)` bên dưới sẽ kiểm tra vấn đề này**","metadata":{}},{"cell_type":"markdown","source":"### **a. Timestamp ingest**","metadata":{}},{"cell_type":"code","source":"from datetime import datetime\n\n# Ghi lại thời điểm ingest hiện tại\ningest_timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\nprint(\"Ingest timestamp:\", ingest_timestamp)\n\n# Option: gắn timestamp vào toàn bộ DataFrame\ncodebook['ingest_timestamp'] = ingest_timestamp\ndf_full_7_9['ingest_timestamp'] = ingest_timestamp","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.678940Z","iopub.execute_input":"2025-06-18T08:46:32.679300Z","iopub.status.idle":"2025-06-18T08:46:32.708293Z","shell.execute_reply.started":"2025-06-18T08:46:32.679277Z","shell.execute_reply":"2025-06-18T08:46:32.707398Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **b. Data version**","metadata":{}},{"cell_type":"code","source":"# Version theo thời gian ingest\ndata_version = \"v_\" + datetime.now().strftime(\"%Y_%m_%d_%H_%M\")\nprint(\"Data version:\", data_version)\n\n# Có thể thêm vào metadata hoặc tên file lưu\ncodebook['data_version'] = data_version\ndf_full_7_9['data_version'] = data_version","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.709262Z","iopub.execute_input":"2025-06-18T08:46:32.709513Z","iopub.status.idle":"2025-06-18T08:46:32.736191Z","shell.execute_reply.started":"2025-06-18T08:46:32.709493Z","shell.execute_reply":"2025-06-18T08:46:32.735280Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧾 **1.2 Metadata Logging**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra mối quan hệ dữ liệu (`df_full_7_9` và `codebook`)\n","metadata":{}},{"cell_type":"markdown","source":"\nTrước khi phân tích dữ liệu, việc đảm bảo rằng **tất cả biến (columns) trong `df_full_7_9` có được định nghĩa trong `codebook`** là rất quan trọng.  \n\n- **`codebook`**: Chứa danh sách các biến có ý nghĩa, đóng vai trò như một tài liệu tham chiếu về cấu trúc dữ liệu (metadata).  \n- **`df_full_7_9`**: Chứa dữ liệu thực tế được thu thập, tất cả các cột phải được định nghĩa trong `codebook` để đảm bảo tính đúng đắn, khách quan khi phân tích.  \n\nSau đây tiến hành kiểm tra xem có biến nào trong `df_full_7_9` nhưng bị thiếu trong `codebook` không? Điều này giúp:  \n✔️ Đảm bảo dữ liệu không bị thiếu sót trước khi phân tích.  \n✔️ Xác định các cột không cần thiết hoặc đã đặt sai tên.  \n✔️ Chuẩn hóa dữ liệu để đảm bảo tính nhất quán.  \n\n\n---","metadata":{}},{"cell_type":"code","source":"def check_variables_in_information(codebook, df_full_7_9):\n    \"\"\"\n    Kiểm tra trong df_full_7_9 có bao nhiêu biến được định nghĩa trong codebook,\n    và bao nhiêu biến không được định nghĩa (unexpected), bỏ qua hai cột kỹ thuật.\n\n    Args:\n    - codebook: DataFrame chứa danh sách các biến hợp lệ trong cột 'Attributions'\n    - df_full_7_9: DataFrame chứa dữ liệu thực tế\n\n    Returns:\n    - None (chỉ in ra thông tin chi tiết)\n    \"\"\"\n    # Tập biến hợp lệ đã định nghĩa\n    defined_variables = set(codebook[\"Attributions\"].str.strip())\n\n    # Các cột cần bỏ qua trong quá trình kiểm tra\n    ignored_columns = {\"data_version\", \"ingest_timestamp\"}\n\n    # Tập biến thực tế trong dữ liệu (sau khi loại bỏ các cột bị bỏ qua)\n    info_variables = set(df_full_7_9.columns.str.strip()) - ignored_columns\n\n    # Biến có trong df_full_7_9 mà cũng được định nghĩa\n    matched_variables = info_variables & defined_variables\n\n    # Biến có trong df_full_7_9 nhưng KHÔNG được định nghĩa\n    unexpected_variables = info_variables - defined_variables\n\n    print(f\"Tổng số biến trong df_full_7_9 (bỏ qua cột kỹ thuật): {len(info_variables)}\")\n    print(f\"Số biến được định nghĩa trong codebook: {len(matched_variables)}\")\n    print(f\"Số biến KHÔNG được định nghĩa trong codebook: {len(unexpected_variables)}\")\n\n    print(\"\\nDanh sách biến ĐƯỢC định nghĩa:\")\n    print(sorted(matched_variables))\n\n    if unexpected_variables:\n        print(\"\\n🔺 Danh sách biến KHÔNG ĐƯỢC định nghĩa:\")\n        print(sorted(unexpected_variables))\n    else:\n        print(\"\\n🎉 Không có biến nào bất ngờ! Mọi thứ đều đúng chuẩn định nghĩa.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.737217Z","iopub.execute_input":"2025-06-18T08:46:32.737536Z","iopub.status.idle":"2025-06-18T08:46:32.757085Z","shell.execute_reply.started":"2025-06-18T08:46:32.737506Z","shell.execute_reply":"2025-06-18T08:46:32.756015Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_variables_in_information(codebook, df_full_7_9)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.758219Z","iopub.execute_input":"2025-06-18T08:46:32.758579Z","iopub.status.idle":"2025-06-18T08:46:32.785565Z","shell.execute_reply.started":"2025-06-18T08:46:32.758554Z","shell.execute_reply":"2025-06-18T08:46:32.784589Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔎 **1.3 Data Lineage**","metadata":{}},{"cell_type":"markdown","source":"### 🌟 Tổng quan về Lineage của project","metadata":{}},{"cell_type":"markdown","source":"| Thành phần | Chi tiết |\n|------------|----------|\n| 🎓 **Nguồn dữ liệu** | - `delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv` và `not_delay_7_9_CONDITION_PRODUCT_SUPPLIER.csv`<br>- `Sample codebook of Delay Prediction task.xlsx`<br><br>👉 Được cung cấp bởi Thầy Trần Quốc Khánh – Trường Đại học CNTT, ĐHQG-HCM |\n| 📥 **Data Ingestion** | - Thêm cột `Timestamp_Ingest` để đánh dấu thời gian ingestion<br>- Thêm cột `Data version` để theo dõi phiên bản |\n| 🛠️ **Schema Reconstruct** | - Không xử lý thêm  |\n| 📃 **Data Contracts** | - Thiết lập thỏa thuận sử dụng dữ liệu  |","metadata":{}},{"cell_type":"markdown","source":"## 🔐 **1.4 Governance & Access Control**","metadata":{}},{"cell_type":"markdown","source":"### 🧭 4 trụ cột vàng trong Governance của project","metadata":{}},{"cell_type":"markdown","source":"|Trụ cột|Mô tả|Ứng dụng trong project này|\n|---|---|---|\n|**Data Ownership**|Người “chính chủ” của dữ liệu|**Thầy Trần Quốc Khánh** – giảng viên hướng dẫn, người chịu trách nhiệm chính|\n|**Data Stewardship**|Người trực tiếp làm việc và bảo trì dữ liệu|**Lương Đắc Nguyên** – sinh viên thực hành, xử lý và ghi chú biến đổi|\n|**Data Quality**|Dữ liệu có sạch, đúng, đầy đủ?|Dữ liệu được chuẩn hóa tên thuộc tính, đã ghi lại version, timestamp nhưng chưa xử lý thiếu, giá trị trùng lặp|\n|**Data Policy & Rules**|Luật lệ rõ ràng trong sử dụng dữ liệu|Cam kết sử dụng cho học tập, không chia sẻ ngoài phạm vi lớp học|","metadata":{}},{"cell_type":"markdown","source":"### 🛡️ Access Control","metadata":{}},{"cell_type":"markdown","source":"|Vai trò|Quyền hạn|Ứng dụng thực tế|\n|---|---|---|\n|**Owner (Thầy Khánh)**|Toàn quyền sở hữu, cấp quyền, giám sát|Được cấp phát dữ liệu gốc & thiết lập chính sách|\n|**Steward (Sinh viên - Nguyên)**|Đọc & chỉnh sửa dữ liệu local|Toàn quyền xử lý, nhưng **không công khai**, không thay đổi schema tùy tiện|\n|**Khác (bên ngoài)**|Không có quyền truy cập|Không được xem hay sử dụng nếu không có sự cho phép|","metadata":{}},{"cell_type":"markdown","source":"### 📜 Data Contract ","metadata":{}},{"cell_type":"markdown","source":"📝 Cả hai bên đã thống nhất cam kết sau:\n\n- ✅ **Chỉ sử dụng dữ liệu cho học tập**\n- ✅ **Không chia sẻ công khai hoặc phát tán**\n- ✅ **Giữ nguyên cấu trúc schema, không tự ý chỉnh sửa**\n- ✅ **Có ghi lại mọi bước xử lý trong Data Lineage (phần 1.3)**","metadata":{}},{"cell_type":"markdown","source":"## 🥉**1.5 BRONZE Layer Data**","metadata":{}},{"cell_type":"code","source":"import os\n\noutput_dir = '/kaggle/working/Data for practice'\noutput_path = os.path.join(output_dir, 'df_full_7_9.csv')\ndf_full_7_9.to_csv(output_path, index=False)\n\nprint(f\"📄 File CSV đã được lưu tại: {output_path}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:32.786631Z","iopub.execute_input":"2025-06-18T08:46:32.786923Z","iopub.status.idle":"2025-06-18T08:46:50.894719Z","shell.execute_reply.started":"2025-06-18T08:46:32.786893Z","shell.execute_reply":"2025-06-18T08:46:50.893797Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Sau khi hoàn thành các bước chuẩn bị ban đầu, ta đã xây dựng thành công lớp dữ liệu **Bronze** với:\n\n- **Dữ liệu thô:** `delay_full_7_9.csv`  \n- **Tài liệu mô tả (codebook):** `Sample codebook of Delay Prediction task.xlsx`\n\n📌 *Lưu ý:* Dữ liệu thô hiện tại vẫn **chưa được làm sạch hay chuẩn hóa**, chỉ là dạng dữ liệu gốc ban đầu.","metadata":{}},{"cell_type":"markdown","source":"# **2. Data Validation & Profiling**","metadata":{}},{"cell_type":"markdown","source":"## **🧭 2.1 Data Profiling**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra kiểu dữ liệu","metadata":{}},{"cell_type":"code","source":"# In ra tên cột và dtype tương ứng trong DataFrame df_full_7_9\ncodebook[['Attributions', 'Data Type']].head()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:50.895656Z","iopub.execute_input":"2025-06-18T08:46:50.895937Z","iopub.status.idle":"2025-06-18T08:46:50.905711Z","shell.execute_reply.started":"2025-06-18T08:46:50.895911Z","shell.execute_reply":"2025-06-18T08:46:50.904770Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cập nhật giá trị trong cột 'Data Type' nếu Attributions là order date hoặc 'VSD' \ncodebook.loc[codebook['Attributions'] == 'VSD', 'Data Type'] = 'DateTime'\ncodebook.loc[codebook['Attributions'] == 'Order date', 'Data Type'] = 'DateTime'","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:50.906688Z","iopub.execute_input":"2025-06-18T08:46:50.907013Z","iopub.status.idle":"2025-06-18T08:46:50.925479Z","shell.execute_reply.started":"2025-06-18T08:46:50.906989Z","shell.execute_reply":"2025-06-18T08:46:50.924318Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def convert_column_dtypes_from_codebook(df: pd.DataFrame, codebook: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    Chuyển đổi kiểu dữ liệu của các cột trong DataFrame đầu vào dựa trên định nghĩa từ codebook.\n\n    Args:\n        df (pd.DataFrame): DataFrame chứa dữ liệu gốc cần chuyển kiểu dữ liệu (vd: df_full_7_9).\n        codebook (pd.DataFrame): DataFrame định nghĩa kiểu dữ liệu cho từng cột, \n            yêu cầu có các cột:\n                - 'Attributions': tên cột trong df\n                - 'Data Type': kiểu dữ liệu đích (int64, float, object, DateTime...)\n\n    Returns:\n        pd.DataFrame: DataFrame đầu vào sau khi đã cố gắng chuyển đổi kiểu dữ liệu các cột tương ứng.\n\n    In ra:\n        - ✅ Với mỗi cột đổi thành công → có thể in ra tên cột và kiểu đã chuyển\n        - ❌ Với mỗi cột lỗi → in ra tên cột, kiểu định chuyển, và lý do lỗi (exception message)\n\n    Notes:\n        - Các kiểu dữ liệu hỗ trợ gồm: int, float, object/string, DateTime\n        - Với kiểu int sử dụng kiểu nullable 'Int64' của pandas để giữ lại giá trị NaN nếu có\n        - Nếu kiểu dữ liệu không được nhận diện hoặc lỗi ép kiểu → cột sẽ bị bỏ qua\n\n    Ví dụ dùng:\n        df_full_7_9 = convert_column_dtypes_from_codebook(df_full_7_9, codebook_full)\n    \"\"\"\n    for _, row in codebook.iterrows():\n        col = row['Attributions']\n        dtype = row['Data Type']\n\n        try:\n            if dtype.lower() in ['int', 'int64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('Int64')\n            elif dtype.lower() in ['float', 'float64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('float')\n            elif dtype.lower() in ['object', 'string']:\n                df[col] = df[col].astype(str)\n            elif dtype.lower() in ['datetime', 'datetime64', 'datetime64[ns]']:\n                try:\n                    # Cố gắng parse kiểu ISO8601 trước\n                    df[col] = pd.to_datetime(df[col], format='ISO8601', errors='raise')\n                    print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (ISO8601)\")\n                except Exception as e_iso:\n                    try:\n                        # Cố gắng với format kiểu Excel: \"4/1/2022  12:00:00 AM\"\n                        df[col] = pd.to_datetime(df[col], format=\"%m/%d/%Y %I:%M:%S %p\", errors='raise')\n                        print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (Excel-style)\")\n                    except Exception as e_excel:\n                        try:\n                            # Cuối cùng là mixed\n                            df[col] = pd.to_datetime(df[col], format='mixed', errors='raise')\n                            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (mixed format)\")\n                        except Exception as e_mixed:\n                            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → DateTime | ISO8601: {e_iso} | Excel: {e_excel} | mixed: {e_mixed}\")\n\n            else:\n                print(f\"Không rõ kiểu dữ liệu '{dtype}' cho cột '{col}', bỏ qua.\")\n                continue\n\n            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → {dtype}\")\n        \n        except Exception as e:\n            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → {dtype} | Lý do: {e}\")\n\n    return df","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:50.926430Z","iopub.execute_input":"2025-06-18T08:46:50.927118Z","iopub.status.idle":"2025-06-18T08:46:50.947545Z","shell.execute_reply.started":"2025-06-18T08:46:50.927087Z","shell.execute_reply":"2025-06-18T08:46:50.946535Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9 = convert_column_dtypes_from_codebook(df_full_7_9, codebook)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:50.948662Z","iopub.execute_input":"2025-06-18T08:46:50.949003Z","iopub.status.idle":"2025-06-18T08:46:51.902682Z","shell.execute_reply.started":"2025-06-18T08:46:50.948979Z","shell.execute_reply":"2025-06-18T08:46:51.901779Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Phân rã thuộc tính kiểu datetime, time**","metadata":{}},{"cell_type":"code","source":"# Tách Order date\ndf_full_7_9['order_year'] = df_full_7_9['Order date'].dt.year\ndf_full_7_9['order_month'] = df_full_7_9['Order date'].dt.month\ndf_full_7_9['order_dayofweek'] = df_full_7_9['Order date'].dt.dayofweek\ndf_full_7_9['is_order_weekend'] = df_full_7_9['order_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tách VSD\ndf_full_7_9['vsd_year'] = df_full_7_9['VSD'].dt.year\ndf_full_7_9['vsd_month'] = df_full_7_9['VSD'].dt.month\ndf_full_7_9['vsd_dayofweek'] = df_full_7_9['VSD'].dt.dayofweek\ndf_full_7_9['is_vsd_weekend'] = df_full_7_9['vsd_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tính khoảng cách giữa VSD và Order date (nếu hợp lý về logic nghiệp vụ)\ndf_full_7_9['days_to_vsd'] = (df_full_7_9['VSD'] - df_full_7_9['Order date']).dt.days","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:51.903598Z","iopub.execute_input":"2025-06-18T08:46:51.903892Z","iopub.status.idle":"2025-06-18T08:46:52.955882Z","shell.execute_reply.started":"2025-06-18T08:46:51.903846Z","shell.execute_reply":"2025-06-18T08:46:52.955083Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9['vsd_year'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:52.970668Z","iopub.execute_input":"2025-06-18T08:46:52.971004Z","iopub.status.idle":"2025-06-18T08:46:52.986329Z","shell.execute_reply.started":"2025-06-18T08:46:52.970982Z","shell.execute_reply":"2025-06-18T08:46:52.985493Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`REASON_CD` → float64 \n>Ý nghĩa cột: Mã lý do đơn hàng bị delay.\n\n>Nhận định: Những dòng có chứa giá trị thường ứng với label = 1, tức đơn hàng bị giao trễ. Điều này cho thấy REASON_CD là một thông tin chỉ xuất hiện sau khi đơn hàng đã bị delay, hay nói cách khác là dữ liệu thuộc tương lai so với thời điểm ta cần dự đoán.\n\n>Hành động: Vì không thể sử dụng dữ liệu tương lai trong quá trình huấn luyện mô hình (training), nên cột REASON_CD sẽ bị loại bỏ khỏi dataset.","metadata":{}},{"cell_type":"code","source":"print(df_full_7_9['REASON_CD'].unique())  ","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:52.987233Z","iopub.execute_input":"2025-06-18T08:46:52.987535Z","iopub.status.idle":"2025-06-18T08:46:53.011835Z","shell.execute_reply.started":"2025-06-18T08:46:52.987513Z","shell.execute_reply":"2025-06-18T08:46:53.010850Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tuy nhiên vì cột này là dữ liệu tương lai, ta sẽ loại bỏ:\ndf_full_7_9 = df_full_7_9.drop(columns=['REASON_CD'])  # loại bỏ hoàn toàn cột REASON_CD\ncodebook = codebook[codebook['Attributions'] != 'REASON_CD']","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:53.012878Z","iopub.execute_input":"2025-06-18T08:46:53.013336Z","iopub.status.idle":"2025-06-18T08:46:53.440037Z","shell.execute_reply.started":"2025-06-18T08:46:53.013313Z","shell.execute_reply":"2025-06-18T08:46:53.439257Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"> Ta chuyển kiểu dữ liệu 1 số trường là khóa ngoại về `object`","metadata":{}},{"cell_type":"code","source":"cols_to_convert = ['CLASSIFY_CD', 'CUST_CD', 'SHIP DECISION NO']\ndf_full_7_9[cols_to_convert] = df_full_7_9[cols_to_convert].astype('object')","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:53.441233Z","iopub.execute_input":"2025-06-18T08:46:53.441510Z","iopub.status.idle":"2025-06-18T08:46:53.625354Z","shell.execute_reply.started":"2025-06-18T08:46:53.441488Z","shell.execute_reply":"2025-06-18T08:46:53.624456Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic của 1 số cột về missing values","metadata":{}},{"cell_type":"code","source":"df_full_7_9.isnull().sum().sort_values(ascending=False)[:7]","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:53.626512Z","iopub.execute_input":"2025-06-18T08:46:53.626844Z","iopub.status.idle":"2025-06-18T08:46:54.486178Z","shell.execute_reply.started":"2025-06-18T08:46:53.626816Z","shell.execute_reply":"2025-06-18T08:46:54.485267Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9['QTUF_RCV_NO'].value_counts().sort_values(ascending=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:54.487728Z","iopub.execute_input":"2025-06-18T08:46:54.488075Z","iopub.status.idle":"2025-06-18T08:46:54.499797Z","shell.execute_reply.started":"2025-06-18T08:46:54.488042Z","shell.execute_reply":"2025-06-18T08:46:54.498783Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9['SOUF_RCV_NO'].value_counts().sort_values(ascending=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:54.501026Z","iopub.execute_input":"2025-06-18T08:46:54.501821Z","iopub.status.idle":"2025-06-18T08:46:54.640424Z","shell.execute_reply.started":"2025-06-18T08:46:54.501794Z","shell.execute_reply":"2025-06-18T08:46:54.639279Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9['Ship Mode'].value_counts().sort_values(ascending=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:54.641331Z","iopub.execute_input":"2025-06-18T08:46:54.641590Z","iopub.status.idle":"2025-06-18T08:46:54.707236Z","shell.execute_reply.started":"2025-06-18T08:46:54.641572Z","shell.execute_reply":"2025-06-18T08:46:54.706289Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy cột `SOUF_RCV_NO` và `Ship Mode` mặc dù đa phần giá trị là `nan` nhưng do cột đang kiểu object nên bị hiểu lầm là chuỗi string. Nên ta sẽ chuyển nó về kiểu float và thay giá trị chuỗi `nan` thành `NAN`","metadata":{}},{"cell_type":"code","source":"# Thay các chuỗi 'nan' (chuỗi chữ, không phải NaN thật sự) thành np.nan\ndf_full_7_9['SOUF_RCV_NO'] = df_full_7_9['SOUF_RCV_NO'].replace('nan', np.nan)\ndf_full_7_9['Ship Mode'] = df_full_7_9['Ship Mode'].replace('nan', np.nan)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:54.708259Z","iopub.execute_input":"2025-06-18T08:46:54.708571Z","iopub.status.idle":"2025-06-18T08:46:54.964704Z","shell.execute_reply.started":"2025-06-18T08:46:54.708549Z","shell.execute_reply":"2025-06-18T08:46:54.963937Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic ẩn của 1 số cột","metadata":{}},{"cell_type":"markdown","source":"🔧 Vấn đề: Cột SO_TIME (Giờ đặt hàng) hiện đang được mã hóa sai kiểu dữ liệu – dưới dạng số nguyên (int), `ví dụ: 115325 → thực chất là 11:53:25`\n\n\nTa cần chuyển đổi các giá trị số này về kiểu thời gian chuẩn (datetime.time) để xử lý chính xác hơn ở các bước phân tích và trích xuất đặc trưng.","metadata":{}},{"cell_type":"code","source":"def convert_to_time(x):\n    try:\n        x_str = str(int(x)).zfill(6)\n        hour = int(x_str[:2])\n        minute = int(x_str[2:4])\n        second = int(x_str[4:6])\n        return time(hour, minute, second)\n    except:\n        return pd.NaT\n\n# Áp dụng\ndf_full_7_9['SO_TIME'] = df_full_7_9['SO_TIME'].apply(convert_to_time)\n\n\n# Kiểm tra kết quả\nprint(\"✅ Đã chuẩn hóa cột 'SO_TIME' về kiểu thời gian thực tế.\")\ndf_full_7_9['SO_TIME'].head()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:54.965747Z","iopub.execute_input":"2025-06-18T08:46:54.966026Z","iopub.status.idle":"2025-06-18T08:46:56.224810Z","shell.execute_reply.started":"2025-06-18T08:46:54.966004Z","shell.execute_reply":"2025-06-18T08:46:56.223901Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đảm bảo cột SO_TIME đã là kiểu datetime.time\n# Nếu chưa: df_full_7_9['SO_TIME'] = df_full_7_9['SO_TIME'].apply(convert_to_time)\n\n# Tạo đặc trưng từ SO_TIME\ndf_full_7_9['so_hour'] = df_full_7_9['SO_TIME'].apply(lambda x: x.hour if pd.notnull(x) else np.nan)\ndf_full_7_9['so_minute'] = df_full_7_9['SO_TIME'].apply(lambda x: x.minute if pd.notnull(x) else np.nan)\ndf_full_7_9['so_second'] = df_full_7_9['SO_TIME'].apply(lambda x: x.second if pd.notnull(x) else np.nan)\n\n# Buổi sáng hay chiều\ndf_full_7_9['is_morning'] = df_full_7_9['so_hour'].apply(lambda x: 1 if x < 12 else 0 if pd.notnull(x) else np.nan)\n\n# Gán time slot theo khung giờ sinh hoạt\ndef assign_time_slot(hour):\n    if pd.isnull(hour): return np.nan\n    elif 0 <= hour < 6: return 'midnight'\n    elif 6 <= hour < 12: return 'morning'\n    elif 12 <= hour < 18: return 'afternoon'\n    else: return 'evening'\n\ndf_full_7_9['time_slot'] = df_full_7_9['so_hour'].apply(assign_time_slot)\n\n# Binned theo khung 4 tiếng\ndf_full_7_9['time_bin'] = pd.cut(df_full_7_9['so_hour'],\n                                 bins=[-1, 3, 7, 11, 15, 19, 23],\n                                 labels=['0-3h', '4-7h', '8-11h', '12-15h', '16-19h', '20-23h'])\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:56.226075Z","iopub.execute_input":"2025-06-18T08:46:56.226485Z","iopub.status.idle":"2025-06-18T08:46:59.606227Z","shell.execute_reply.started":"2025-06-18T08:46:56.226438Z","shell.execute_reply":"2025-06-18T08:46:59.605299Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### b. Thông tin missing và duplicate trên toàn bộ tập dữ liệu","metadata":{}},{"cell_type":"code","source":"def print_data_overview(df, columns=None):\n    if columns is not None:\n        df = df[columns]\n\n    num_rows = df.shape[0]\n    num_cols = df.shape[1]\n    total_cells = num_rows * num_cols\n    percent_missing = df.isnull().sum().sum() / total_cells * 100\n    percent_duplicate = df.duplicated().sum() / num_rows * 100\n\n    print(f\"Tổng quan: {num_rows} dòng, {num_cols} cột\")\n    print(f\"⚠️  {percent_missing:.4f}% missing | 🔁 {percent_duplicate:.4f}% duplicate\\n\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:59.607228Z","iopub.execute_input":"2025-06-18T08:46:59.607540Z","iopub.status.idle":"2025-06-18T08:46:59.613593Z","shell.execute_reply.started":"2025-06-18T08:46:59.607517Z","shell.execute_reply":"2025-06-18T08:46:59.612498Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_full_7_9)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:46:59.614478Z","iopub.execute_input":"2025-06-18T08:46:59.614782Z","iopub.status.idle":"2025-06-18T08:47:03.443989Z","shell.execute_reply.started":"2025-06-18T08:46:59.614756Z","shell.execute_reply":"2025-06-18T08:47:03.443092Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### c. Các giá trị thống kê, đo lường độ Trung tâm và Phân Tán của dữ liệu","metadata":{}},{"cell_type":"markdown","source":">**Danh sách các cột mang ý nghĩa thống kê (Không bao gồm khóa ngoại, khóa chính, code), lưu ý kiểu dữ liệu của một số cột đã được xử lý thủ công**","metadata":{}},{"cell_type":"code","source":"print(df_full_7_9.select_dtypes(exclude='object').columns)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:03.444891Z","iopub.execute_input":"2025-06-18T08:47:03.445277Z","iopub.status.idle":"2025-06-18T08:47:03.606246Z","shell.execute_reply.started":"2025-06-18T08:47:03.445254Z","shell.execute_reply":"2025-06-18T08:47:03.605333Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9.describe()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:03.607206Z","iopub.execute_input":"2025-06-18T08:47:03.607466Z","iopub.status.idle":"2025-06-18T08:47:05.110164Z","shell.execute_reply.started":"2025-06-18T08:47:03.607447Z","shell.execute_reply":"2025-06-18T08:47:05.108501Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧪 **2.2 Data Validation – Kiểm tra chất lượng & chuẩn đầu vào**","metadata":{}},{"cell_type":"code","source":"df_full_7_9.isnull().sum().sort_values(ascending=False)[:7]","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:05.111155Z","iopub.execute_input":"2025-06-18T08:47:05.111527Z","iopub.status.idle":"2025-06-18T08:47:06.058709Z","shell.execute_reply.started":"2025-06-18T08:47:05.111503Z","shell.execute_reply":"2025-06-18T08:47:06.057766Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9.duplicated().sum()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:06.059808Z","iopub.execute_input":"2025-06-18T08:47:06.060183Z","iopub.status.idle":"2025-06-18T08:47:08.896988Z","shell.execute_reply.started":"2025-06-18T08:47:06.060155Z","shell.execute_reply":"2025-06-18T08:47:08.896200Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_7_9.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_7_9)) * 100\n\n# Sắp xếp và in ra top 7 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(7)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:08.897804Z","iopub.execute_input":"2025-06-18T08:47:08.898107Z","iopub.status.idle":"2025-06-18T08:47:09.836818Z","shell.execute_reply.started":"2025-06-18T08:47:08.898085Z","shell.execute_reply":"2025-06-18T08:47:09.836034Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_full_7_9)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:09.837710Z","iopub.execute_input":"2025-06-18T08:47:09.838009Z","iopub.status.idle":"2025-06-18T08:47:13.613071Z","shell.execute_reply.started":"2025-06-18T08:47:09.837979Z","shell.execute_reply":"2025-06-18T08:47:13.612027Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dữ liệu trước khi đưa vào mô hình cần được kiểm định cẩn thận để đảm bảo chất lượng và độ tin cậy. Dưới đây là những kiểm định đã được thực hiện trên tập dữ liệu hiện tại:\n\n| **Kiểm định**               | **Phát hiện & Xử lý**                                                                                                                                   |\n|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|\n| **Schema Validation**       | ✅ Đã rà soát và phát hiện một số cột có kiểu dữ liệu chưa phù hợp: `Order_date`, `VSD` hiện là *object*, đã chuyển về *datetime*. Một số cột như `CLASSIFY_CD`, `GLOBAL_NO`, `CUST_CD` đã chuẩn hóa về kiểu *object*. |\n| **Null / Missing Constraints** | ⚠️ Tỷ lệ thiếu giá trị tổng thể là **7.61%**. Hai cột có missing gần như toàn bộ là `QTUF_RCV_NO` (**99.87%**) và `SOUF_RCV_NO` (**96.6%**) → cần cân nhắc loại bỏ hoặc xử lý đặc biệt. |\n| **Unique Constraint**       | 🔁 Không có giá trị trùng lặp.                                                  |\n| **Range Checks**            | 📏 Đang trong quá trình kiểm tra các cột số liệu để đảm bảo nằm trong khoảng hợp lý. Ví dụ: các cột về thời gian, số lượng, mã vùng cần được kiểm soát kỹ.                                   |\n| **Constant/Skewed Feature Detection** | 📉 Một số cột như `label` chỉ có duy nhất một giá trị hoặc phân bố cực lệch → khi thực hiện huấn luyện model cần các kỹ thuật để cân bằng dữ liệu.                                                      |\n| **Type Inference**          | 🤖 Kết hợp kiểm tra thủ công và inference tự động để phát hiện các kiểu dữ liệu chưa chính xác hoặc bị ingest sai.                                                                    |\n| **Regex Pattern Checks**    | ⌨️ Đã rà soát các cột định danh như mã khách hàng, mã phân loại,... để đảm bảo định dạng đúng chuẩn.                                                                         |","metadata":{}},{"cell_type":"markdown","source":"## 👁️👁️ 2.3 EDA - Phân tích thăm dò","metadata":{}},{"cell_type":"markdown","source":"### 2.3.1 ✅ Tổng quan","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra mối quan hệ giữa `label` và `order date`","metadata":{}},{"cell_type":"code","source":"# Chuyển 'Order date' làm index\ndf_plot = df_full_7_9.set_index('Order date')\n# Resample theo ngày (có thể thay bằng 'W' cho tuần hoặc 'M' cho tháng nếu muốn smooth hơn)\nlabel_1_by_day = df_plot[df_plot['label'] == 1].resample('D').size()\n\nplt.figure(figsize=(14, 6))\nplt.plot(label_1_by_day.index, label_1_by_day.values, color='red', label='Số lượng label = 1')\n\nplt.title('Số lần xuất hiện label = 1 theo thời gian')\nplt.xlabel('Thời gian')\nplt.ylabel('Tần suất')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:13.614163Z","iopub.execute_input":"2025-06-18T08:47:13.614425Z","iopub.status.idle":"2025-06-18T08:47:15.034255Z","shell.execute_reply.started":"2025-06-18T08:47:13.614404Z","shell.execute_reply":"2025-06-18T08:47:15.033370Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như tần xuất đơn hàng bị delay chủ yếu vào giữa các tuần, với một số ngày có sự tăng đột biến như ngày 15/7 ngày 1/8 ngày 8/8 ngày 2/9","metadata":{}},{"cell_type":"code","source":"# Đảm bảo cột 'Order date' là datetime\ndf_full_7_9['Order date'] = pd.to_datetime(df_full_7_9['Order date'])\n\ndf_plot = df_full_7_9.set_index('Order date')\n\n# Resample theo ngày (có thể đổi 'D' -> 'W' hoặc 'M' nếu cần mượt hơn)\nlabel_1_by_day = df_plot[df_plot['label'] == 1].resample('D').size()\nlabel_0_by_day = df_plot[df_plot['label'] == 0].resample('D').size()\n\nplt.figure(figsize=(14, 6))\nplt.plot(label_1_by_day.index, label_1_by_day.values, color='red', label='Label = 1')\nplt.plot(label_0_by_day.index, label_0_by_day.values, color='blue', label='Label = 0')\n\nplt.title('Tần suất label = 1 (đỏ) và label = 0 (xanh) theo thời gian')\nplt.xlabel('Thời gian')\nplt.ylabel('Số lượng')\nplt.legend()\nplt.grid(True)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:15.035248Z","iopub.execute_input":"2025-06-18T08:47:15.035590Z","iopub.status.idle":"2025-06-18T08:47:22.328755Z","shell.execute_reply.started":"2025-06-18T08:47:15.035565Z","shell.execute_reply":"2025-06-18T08:47:22.327705Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Phân tích định danh đơn hàng","metadata":{}},{"cell_type":"code","source":"len(df_full_7_9['GLOBAL_NO'])","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:22.329696Z","iopub.execute_input":"2025-06-18T08:47:22.329982Z","iopub.status.idle":"2025-06-18T08:47:22.336172Z","shell.execute_reply.started":"2025-06-18T08:47:22.329962Z","shell.execute_reply":"2025-06-18T08:47:22.335215Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_full_7_9['GLOBAL_NO'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:22.337105Z","iopub.execute_input":"2025-06-18T08:47:22.337405Z","iopub.status.idle":"2025-06-18T08:47:23.577994Z","shell.execute_reply.started":"2025-06-18T08:47:22.337383Z","shell.execute_reply":"2025-06-18T08:47:23.577215Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy top 50 GLOBAL_NO xuất hiện nhiều nhất\ntop_50_global_no = df_full_7_9['GLOBAL_NO'].value_counts().head(50)\n\nglobal_no_labels = df_full_7_9.groupby('GLOBAL_NO')['label'].min()\n\ncolors = ['red' if global_no_labels[global_no] == 1 else 'blue' for global_no in top_50_global_no.index]\n\nplt.figure(figsize=(18, 8))\nbars = plt.bar(top_50_global_no.index.astype(str), top_50_global_no.values, color=colors)\n\n# Gắn nhãn số lượng lên từng cột\nfor i, bar in enumerate(bars):\n    height = bar.get_height()\n    plt.text(bar.get_x() + bar.get_width()/2, height + 0.5,\n             str(int(height)), ha='center', va='bottom', fontsize=8, fontweight='bold')\n\nplt.title('Top 50 GLOBAL_NO - Phân màu theo Label (1: đỏ, 0: xanh)', fontsize=14)\nplt.xlabel('GLOBAL_NO')\nplt.ylabel('Số lần xuất hiện')\nplt.xticks(rotation=75, ha='right', fontsize=8)\nplt.grid(axis='y')\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:23.579050Z","iopub.execute_input":"2025-06-18T08:47:23.580156Z","iopub.status.idle":"2025-06-18T08:47:28.078495Z","shell.execute_reply.started":"2025-06-18T08:47:23.580128Z","shell.execute_reply":"2025-06-18T08:47:28.077135Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tương tự, Global No cũng là khóa chính","metadata":{}},{"cell_type":"markdown","source":"#### Top 10 SUPPLIER_CD có nhiều đơn nhất","metadata":{}},{"cell_type":"code","source":"grouped = df_full_7_9.groupby(['SUPPLIER_CD', 'label']).size().unstack(fill_value=0)\n\n\ntop10_suppliers = grouped.sum(axis=1).sort_values(ascending=False).head(20)\n\ntop_grouped = grouped.loc[top10_suppliers.index]\nplt.figure(figsize=(10, 6))\n\nplt.bar(top_grouped.index.astype(str), top_grouped[0], label='Label 0', color='lightcoral')\nplt.bar(top_grouped.index.astype(str), top_grouped[1], bottom=top_grouped[0], label='Label 1', color='cornflowerblue')\n\nplt.xlabel('SUPPLIER_CD')\nplt.ylabel('Số đơn hàng')\nplt.title('Stacked Bar Chart: Top 20 Supplier theo Label')\nplt.xticks(rotation=45)\nplt.legend(title='Label')\nplt.tight_layout()\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:28.079584Z","iopub.execute_input":"2025-06-18T08:47:28.079889Z","iopub.status.idle":"2025-06-18T08:47:28.529286Z","shell.execute_reply.started":"2025-06-18T08:47:28.079841Z","shell.execute_reply":"2025-06-18T08:47:28.528018Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy nhà cung cấp với mã code 3186 có tỉ lệ đơn hàng delay rất lớn so với các nhà cung cấp khác. Nhà cung cấp 3186 và 8484 cũng có số lượng đơn hàng bị delay có thể quan sát qua biểu đồ.","metadata":{}},{"cell_type":"markdown","source":"### 2.3.2 🚛 Vận chuyển","metadata":{}},{"cell_type":"markdown","source":"#### So sánh DIRECT SHIP FLG, DELI_DIV, Ship Mode","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport matplotlib.pyplot as plt\n\n# Các cột cần vẽ\ncols = ['DIRECT SHIP FLG', 'DELI_DIV', 'Ship Mode']\nlabel_col = 'label'\n\n# Tạo Figure với 3 subplot nằm trên 1 dòng\nfig, axes = plt.subplots(1, 3, figsize=(18, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm và đếm theo giá trị + label\n    grouped = df_full_7_9.groupby([col, label_col]).size().unstack(fill_value=0)\n\n    # Đảm bảo có cả label 0 và 1\n    if 0 not in grouped.columns:\n        grouped[0] = 0\n    if 1 not in grouped.columns:\n        grouped[1] = 0\n\n    grouped = grouped[[0, 1]]  # Giữ thứ tự\n\n    # Tính số lượng giá trị unique\n    n_unique = df_full_7_9[col].nunique()\n\n    # Vẽ stacked bar chart\n    ax.bar(grouped.index.astype(str), grouped[0], label='Label 0', color='lightcoral')\n    ax.bar(grouped.index.astype(str), grouped[1], bottom=grouped[0], label='Label 1', color='steelblue')\n    \n    # Gắn tiêu đề có số lượng unique\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xticklabels(grouped.index.astype(str), rotation=45, ha='right')\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\n# Gán nhãn chung\nfig.suptitle('Phân phối Label theo DIRECT_SHIP_FLG, DELI_DIV và Ship Mode', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:28.530464Z","iopub.execute_input":"2025-06-18T08:47:28.531380Z","iopub.status.idle":"2025-06-18T08:47:29.522010Z","shell.execute_reply.started":"2025-06-18T08:47:28.531345Z","shell.execute_reply":"2025-06-18T08:47:29.520721Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối theo label của `DIRECT SHIP FLG` có vẻ khá cân bằng, điều này khá tương tự với trường hợp của `Ship mode` và `DELI_DIV`","metadata":{}},{"cell_type":"markdown","source":"### 2.3.3 📦 Sản phẩm","metadata":{}},{"cell_type":"markdown","source":"### Kiểm tra tỷ lệ trễ của hàng nặng (HEAVY_FLG), hàng nguy hiểm (HAZARD_FLG), hàng đắt (EXPENSIVE_FLG), loại tồn kho (Stock class)","metadata":{}},{"cell_type":"code","source":"# Các cột flag cần vẽ + 1 cột rời rạc thêm\ncols = ['DIRECT SHIP FLG', 'PACKING RANK', 'SPECIAL DIV', 'Stock class']\nlabel_col = 'label'\n\n# Tạo Figure với 4 subplot nằm trên 1 dòng\nfig, axes = plt.subplots(1, 4, figsize=(24, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm và đếm theo giá trị + label\n    grouped = df_full_7_9.groupby([col, label_col]).size().unstack(fill_value=0)\n\n    # Đảm bảo có cả label 0 và 1\n    for lbl in [0, 1]:\n        if lbl not in grouped.columns:\n            grouped[lbl] = 0\n\n    grouped = grouped[[0, 1]]  # Đảm bảo đúng thứ tự\n\n    # Tính số lượng giá trị unique\n    n_unique = df_full_7_9[col].nunique()\n\n    # Vẽ stacked bar chart\n    x = grouped.index.astype(str)\n    bar1 = ax.bar(x, grouped[0], label='Label 0', color='lightcoral')\n    bar2 = ax.bar(x, grouped[1], bottom=grouped[0], label='Label 1', color='steelblue')\n    \n    # Gắn số lượng lên đầu mỗi cột\n    for j in range(len(x)):\n        total = grouped[0].iloc[j] + grouped[1].iloc[j]\n        ax.text(j, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n    # Gắn tiêu đề có số lượng unique\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xticklabels(x, rotation=30, ha='center')\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\n# Gán nhãn chung\nfig.suptitle('Phân phối Label theo các cột Flag và Stock Class', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:29.522847Z","iopub.execute_input":"2025-06-18T08:47:29.523190Z","iopub.status.idle":"2025-06-18T08:47:30.492903Z","shell.execute_reply.started":"2025-06-18T08:47:29.523167Z","shell.execute_reply":"2025-06-18T08:47:30.491876Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `label` theo các cờ về các thuộc tính không phản ánh 1 xu hướng rõ ràng nào để đưa ra kết luận.","metadata":{}},{"cell_type":"markdown","source":"#### Khối lượng `WEIGHT PER PIECE - Khối lượng từng món hàng` ảnh hưởng ra sao?","metadata":{}},{"cell_type":"code","source":"top_weights = df_full_7_9['WEIGHT PER PIECE'].value_counts().head(30).index\n\n# B2: Lọc dữ liệu theo top_weights\ndf_top = df_full_7_9[df_full_7_9['WEIGHT PER PIECE'].isin(top_weights)]\n\n# B3: Nhóm theo weight và label\ngrouped = df_top.groupby(['WEIGHT PER PIECE', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo có cả label 0 và 1\nfor lbl in [0, 1]:\n    if lbl not in grouped.columns:\n        grouped[lbl] = 0\n\ngrouped = grouped[[0, 1]]  # Đúng thứ tự\n\n# B4: Vẽ stacked bar chart\nplt.figure(figsize=(12, 6))\nx = grouped.index.astype(str)\n\nbar1 = plt.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\nbar2 = plt.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n\n# Thêm số lượng tổng mỗi cột\nfor i in range(len(x)):\n    total = grouped[0].iloc[i] + grouped[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\nplt.title('Phân phối Label theo các trọng lượng phổ biến nhất')\nplt.xlabel('WEIGHT PER PIECE')\nplt.ylabel('Số lượng đơn hàng')\nplt.xticks(rotation=45)\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:30.493975Z","iopub.execute_input":"2025-06-18T08:47:30.494300Z","iopub.status.idle":"2025-06-18T08:47:31.260301Z","shell.execute_reply.started":"2025-06-18T08:47:30.494254Z","shell.execute_reply":"2025-06-18T08:47:31.259014Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `Khối lượng từng món hàng` theo label khá giống nhau. Ta biết thêm 1 thông tin là đa phần Khối lượng của từng món hàng chủ yếu có khối lượng nhỏ","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra phân phối của label với từng loại SHIP MODE","metadata":{}},{"cell_type":"code","source":"top_10_modes = df_full_7_9['Ship Mode'].value_counts().head(20).index\n\n# Lọc dữ liệu chỉ giữ lại các Ship Mode nằm trong top 10\nfiltered_df = df_full_7_9[df_full_7_9['Ship Mode'].isin(top_10_modes)]\n\n# Đếm theo Ship Mode và label\nshipmode_counts = filtered_df.groupby(['Ship Mode', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đúng thứ tự\nshipmode_counts = shipmode_counts.loc[top_10_modes]  # giữ đúng thứ tự top 10\nshipmode_counts = shipmode_counts[[0, 1]]  # label 0 trước, rồi label 1\n\n# Vẽ stacked bar chart\nplt.figure(figsize=(12, 6))\nx = shipmode_counts.index.astype(str)\nbar1 = plt.bar(x, shipmode_counts[0], label='Label 0', color='mediumseagreen')\nbar2 = plt.bar(x, shipmode_counts[1], bottom=shipmode_counts[0], label='Label 1', color='tomato')\n\n# Gắn số lượng lên mỗi cột\nfor i in range(len(x)):\n    total = shipmode_counts[0].iloc[i] + shipmode_counts[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Trang trí\nplt.title('Phân phối Ship Mode theo Label (Top 10 phổ biến nhất)', fontsize=14)\nplt.xlabel('Ship Mode')\nplt.ylabel('Số lượng đơn hàng')\nplt.xticks(rotation=45)\nplt.grid(axis='y', linestyle='--', alpha=0.5)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:31.261543Z","iopub.execute_input":"2025-06-18T08:47:31.261827Z","iopub.status.idle":"2025-06-18T08:47:32.260915Z","shell.execute_reply.started":"2025-06-18T08:47:31.261806Z","shell.execute_reply":"2025-06-18T08:47:32.259842Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như không có mối quan hệ nào giữa `label` và `SHIP MODE`","metadata":{}},{"cell_type":"markdown","source":"### 2.3.4 📅 Thời gian","metadata":{}},{"cell_type":"markdown","source":"#### Phân tích theo SO_DAY_OF_WEEK và SO_DAY_OF_MONTH","metadata":{}},{"cell_type":"markdown","source":"🔔 **Lưu ý quan trọng về trường `SO_DAY_OF_WEEK`:**\nTrong dữ liệu, cột `SO_DAY_OF_WEEK` chứa các giá trị rời rạc đại diện cho các ngày trong tuần, cụ thể như sau:\n\n* `1` → **Chủ nhật**\n* `2` → **Thứ Bảy**\n* `3` → **Thứ Hai**\n* `4` → **Thứ Ba**\n* `5` → **Thứ Tư**\n* `6` → **Thứ Năm**\n\n❗️**Không có giá trị nào tương ứng với Thứ Sáu** trong dữ liệu này. Đây là một điểm cần đặc biệt lưu ý khi xử lý hoặc trực quan hóa theo ngày trong tuần.\n","metadata":{}},{"cell_type":"code","source":"cols = ['SO_DAY_OF_WEEK', 'SO_DAY_OF_MONTH']\nfig, axes = plt.subplots(1, 2, figsize=(18, 6), sharey=True)\n\nfor i, col in enumerate(cols):\n    ax = axes[i]\n    \n    # Nhóm dữ liệu theo giá trị + label\n    grouped = df_full_7_9.groupby([col, 'label']).size().unstack(fill_value=0)\n\n    # Đảm bảo cả label 0 và 1 đều có\n    for lbl in [0, 1]:\n        if lbl not in grouped.columns:\n            grouped[lbl] = 0\n\n    grouped = grouped[[0, 1]]  # Đúng thứ tự\n    \n    # Tính số lượng giá trị unique\n    n_unique = df_full_7_9[col].nunique()\n    \n    # Vẽ stacked bar chart\n    x = grouped.index.astype(str)\n    bar1 = ax.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\n    bar2 = ax.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n    \n    # In số lượng trên mỗi cột\n    for j in range(len(x)):\n        total = grouped[0].iloc[j] + grouped[1].iloc[j]\n        ax.text(j, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n    # Gắn tiêu đề\n    ax.set_title(f'{col} (Unique: {n_unique})')\n    ax.set_xlabel(col)\n    ax.set_xticks(range(len(x)))\n    ax.set_xticklabels(x, rotation=0)\n    ax.grid(axis='y', linestyle='--', alpha=0.6)\n\nfig.suptitle('Phân phối Đơn hàng theo Ngày trong tuần & Ngày trong tháng', fontsize=16)\nfig.text(0.5, 0.04, 'Giá trị ngày', ha='center')\nfig.text(0.04, 0.5, 'Số lượng đơn hàng', va='center', rotation='vertical')\n\n# Thêm chú thích\nhandles, labels = axes[0].get_legend_handles_labels()\nfig.legend(handles, labels, loc='upper right')\n\nplt.tight_layout(rect=[0.03, 0.05, 0.95, 0.95])\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:32.261985Z","iopub.execute_input":"2025-06-18T08:47:32.262430Z","iopub.status.idle":"2025-06-18T08:47:33.245388Z","shell.execute_reply.started":"2025-06-18T08:47:32.262398Z","shell.execute_reply":"2025-06-18T08:47:33.244489Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy phân phối của `delay` và `not delay` khá cân bằng giữa các ngày trong tuần và các ngày trong tháng.","metadata":{}},{"cell_type":"markdown","source":"####  Phân tích mối quan hệ giữa giờ đặt hàng (SO_TIME) và label (trễ/không trễ)","metadata":{}},{"cell_type":"code","source":"# Bước 1: Tính SO_HOUR tạm thời, không thêm vào df_full_7_9\ntemp_df = df_full_7_9.copy()\ntemp_df['SO_HOUR'] = temp_df['SO_TIME'].apply(lambda x: x.hour if pd.notna(x) else None)\n\n# Bước 2: Group theo giờ và label\nhourly_counts = temp_df.groupby(['SO_HOUR', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đủ 24 giờ (kể cả giờ không có đơn)\nall_hours = pd.Series(range(0, 24), name='SO_HOUR')\nhourly_counts = hourly_counts.reindex(all_hours, fill_value=0)\n\n# Bước 3: Vẽ stacked bar chart\nplt.figure(figsize=(14, 6))\nbars_0 = plt.bar(hourly_counts.index, hourly_counts[0], label='Không trễ (label=0)', color='mediumseagreen')\nbars_1 = plt.bar(hourly_counts.index, hourly_counts[1], bottom=hourly_counts[0], label='Trễ (label=1)', color='tomato')\n\n# Gắn số lượng tổng lên đỉnh\nfor i in hourly_counts.index:\n    total = hourly_counts.loc[i, 0] + hourly_counts.loc[i, 1]\n    if total > 0:\n        plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Cài đặt biểu đồ\nplt.xlabel('Giờ đặt hàng (0–23h)')\nplt.ylabel('Số đơn hàng')\nplt.title('Phân phối đơn hàng theo giờ đặt (SO_TIME) và Label (Trễ / Không trễ)')\nplt.xticks(range(24))\nplt.legend()\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.tight_layout()\nplt.show()\n","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:33.246441Z","iopub.execute_input":"2025-06-18T08:47:33.246734Z","iopub.status.idle":"2025-06-18T08:47:35.507068Z","shell.execute_reply.started":"2025-06-18T08:47:33.246701Z","shell.execute_reply":"2025-06-18T08:47:35.506141Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Mối quan hệ giữa giờ đặt hàng (SO_TIME) và label (trễ/không trễ) khá bình thường, phân bố đều","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra số lượng các ngày lễ/nghỉ (Consider count hodiday Saturday) có ảnh hưởng gì không?","metadata":{}},{"cell_type":"code","source":"col = 'Consider count hodiday Saturday'\n\n# Gom nhóm theo số ngày đặc biệt + label\ngrouped = df_full_7_9.groupby([col, 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đủ 2 nhãn label\nfor lbl in [0, 1]:\n    if lbl not in grouped.columns:\n        grouped[lbl] = 0\n\ngrouped = grouped[[0, 1]]  # đúng thứ tự\n\n# Lấy các giá trị phổ biến đầu tiên (tránh quá dài)\ngrouped = grouped.sort_index().head(10)\n\n# Bắt đầu vẽ\nplt.figure(figsize=(10, 6))\n\nx = grouped.index.astype(str)\nbar1 = plt.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\nbar2 = plt.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n\n# Gắn số lượng lên đầu cột\nfor i in range(len(x)):\n    total = grouped[0].iloc[i] + grouped[1].iloc[i]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Thêm info\nplt.title(f'Phân phối đơn hàng theo Số ngày lễ và ngày nghỉ cuối tuần từ ngày đặt hàng đến ngày giao hàng\\nBiến: \"{col}\" (Unique: {df_full_7_9[col].nunique()})', fontsize=14)\nplt.xlabel('Số ngày lễ + Thứ 7')\nplt.ylabel('Số lượng đơn hàng')\nplt.grid(axis='y', linestyle='--', alpha=0.5)\nplt.legend()\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:35.507994Z","iopub.execute_input":"2025-06-18T08:47:35.508229Z","iopub.status.idle":"2025-06-18T08:47:35.842678Z","shell.execute_reply.started":"2025-06-18T08:47:35.508212Z","shell.execute_reply":"2025-06-18T08:47:35.841599Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy dường như `Số ngày lễ và ngày nghỉ cuối tuần từ ngày đặt hàng đến ngày giao hàng` không có mối quan hệ rõ rệt tới việc đơn hàng có bị delay hay không","metadata":{}},{"cell_type":"markdown","source":"### 🥹 2.3.5 Khác","metadata":{}},{"cell_type":"markdown","source":"#### Kiểm tra các trường có ít giá trị unique - các trường chỉ toàn là 1 giá trị","metadata":{}},{"cell_type":"code","source":"def get_highly_dominant_columns(df, threshold=0.99):\n    \"\"\"\n    Trả về danh sách các cột mà 1 giá trị chiếm ≥ threshold (default 0.99 = 99%)\n    \"\"\"\n    dominant_cols = []\n\n    for col in df.columns:\n        value_counts = df[col].value_counts(normalize=True, dropna=False)\n        if not value_counts.empty:\n            top_freq = value_counts.values[0]\n            if top_freq >= threshold:\n                dominant_val = value_counts.idxmax()\n                dominant_cols.append((col, top_freq, dominant_val))\n\n    return dominant_cols","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:35.843654Z","iopub.execute_input":"2025-06-18T08:47:35.843976Z","iopub.status.idle":"2025-06-18T08:47:35.850510Z","shell.execute_reply.started":"2025-06-18T08:47:35.843954Z","shell.execute_reply":"2025-06-18T08:47:35.849168Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dominant_columns_info = get_highly_dominant_columns(df_full_7_9, threshold=0.99)\n\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:35.851563Z","iopub.execute_input":"2025-06-18T08:47:35.851830Z","iopub.status.idle":"2025-06-18T08:47:39.420607Z","shell.execute_reply.started":"2025-06-18T08:47:35.851810Z","shell.execute_reply":"2025-06-18T08:47:39.419579Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tìm **những cột mà trong đó có một giá trị chiếm >99%**, tức là mất cân bằng dữ liệu cực kỳ cao → có thể không hữu ích cho mô hình hóa hoặc gây nhiễu/overfit, nên ở bước sau ta sẽ xóa.","metadata":{}},{"cell_type":"markdown","source":"# 3. DATA CLEANING & REPAIRING & SLIVER Layer Data","metadata":{}},{"cell_type":"markdown","source":"## 🫳**3.0 Drop meaningless column**","metadata":{}},{"cell_type":"code","source":"# Gọi hàm\ndominant_columns_info = get_highly_dominant_columns(df_full_7_9, threshold=0.99)\n\n# In danh sách trước khi xoá\nprint(\"📌 Các cột sẽ bị drop (1 giá trị chiếm ≥99%):\")\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")\n\n# Thực hiện drop\ncolumns_to_drop = [col for col, _, _ in dominant_columns_info]\ndf_full_7_9 = df_full_7_9.drop(columns=columns_to_drop)\n\nprint(f\"\\n✅ Đã xoá {len(columns_to_drop)} cột. DataFrame giờ còn {df_full_7_9.shape[1]} cột.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:39.421571Z","iopub.execute_input":"2025-06-18T08:47:39.421888Z","iopub.status.idle":"2025-06-18T08:47:43.114752Z","shell.execute_reply.started":"2025-06-18T08:47:39.421838Z","shell.execute_reply":"2025-06-18T08:47:43.113591Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧽 **3.1 Detecting And Handling Missing Values**","metadata":{}},{"cell_type":"code","source":"missing_info = df_full_7_9.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_7_9)) * 100\n\nmissing_info.sort_values(by='missing_count', ascending=False).head(7)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:43.115756Z","iopub.execute_input":"2025-06-18T08:47:43.116054Z","iopub.status.idle":"2025-06-18T08:47:43.889470Z","shell.execute_reply.started":"2025-06-18T08:47:43.116033Z","shell.execute_reply":"2025-06-18T08:47:43.888668Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SOUF_RCV_NO`","metadata":{}},{"cell_type":"markdown","source":"Ta nhận thấy trường `SOUF_RCV_NO - Số tiếp tân không hợp lệ tại thời điểm đặt hàng` bị missing nhiều nhất - 96.6%, đây là dấu hiệu `MNAR - Missing Not At Random - Missing do nội tại của cột đó`. Việc điền missing value bằng các kỹ thuật là điều không thể do logic không rõ ràng và thiếu phần lớn dữ liệu, nên ta sẽ bỏ cột trên.","metadata":{}},{"cell_type":"code","source":"df_full_7_9.drop(columns=['SOUF_RCV_NO'], inplace=True)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:43.890415Z","iopub.execute_input":"2025-06-18T08:47:43.890742Z","iopub.status.idle":"2025-06-18T08:47:44.160921Z","shell.execute_reply.started":"2025-06-18T08:47:43.890715Z","shell.execute_reply":"2025-06-18T08:47:44.159910Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_7_9.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_7_9)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:44.161883Z","iopub.execute_input":"2025-06-18T08:47:44.162320Z","iopub.status.idle":"2025-06-18T08:47:44.905727Z","shell.execute_reply.started":"2025-06-18T08:47:44.162290Z","shell.execute_reply":"2025-06-18T08:47:44.904491Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SHIP DECISION NO` và `SHIP MODE`","metadata":{}},{"cell_type":"code","source":"# Chuẩn bị dữ liệu biểu đồ\ncols_to_check = ['SHIP DECISION NO', 'Ship Mode']\nmissing_stats = []\n\nfor label_value in [0, 1]:\n    subset = df_full_7_9[df_full_7_9['label'] == label_value]\n    total_rows = len(subset)\n\n    for col in cols_to_check:\n        missing_count = subset[col].isnull().sum()\n        missing_percent = (missing_count / total_rows) * 100\n        missing_stats.append({\n            'Label': f'label = {label_value}',\n            'Column': col,\n            'Missing %': missing_percent\n        })\n\n# Tạo DataFrame từ kết quả\nmissing_df = pd.DataFrame(missing_stats)\n\n# Vẽ biểu đồ\nplt.figure(figsize=(8, 5))\nfor col in cols_to_check:\n    subset_df = missing_df[missing_df['Column'] == col]\n    plt.bar(subset_df['Label'], subset_df['Missing %'], label=col)\n\nplt.ylabel('Missing Value (%)')\nplt.title('Tỉ lệ Missing Value theo label')\nplt.legend(title='Column')\nplt.ylim(0, max(missing_df['Missing %']) + 5)\nplt.grid(axis='y', linestyle='--', alpha=0.7)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:44.907019Z","iopub.execute_input":"2025-06-18T08:47:44.907367Z","iopub.status.idle":"2025-06-18T08:47:45.571109Z","shell.execute_reply.started":"2025-06-18T08:47:44.907302Z","shell.execute_reply":"2025-06-18T08:47:45.570290Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_delay_7_9['SHIP DECISION NO'].isnull().sum()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:45.571968Z","iopub.execute_input":"2025-06-18T08:47:45.572277Z","iopub.status.idle":"2025-06-18T08:47:45.578733Z","shell.execute_reply.started":"2025-06-18T08:47:45.572251Z","shell.execute_reply":"2025-06-18T08:47:45.577489Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Có 2 điều bất ngờ:\n- khi giá trị của cột `SHIP DECISION NO` chỉ bị missing chủ yếu ở đơn hàng đó không bị delay. Trái lại nếu đơn hàng bị delay thì tỉ lệ của `SHIP DECISION NO` bị missing là ít - điều này khá giống với bộ 4-6. Điều này gợi ý rằng nếu cột `SHIP DECISION NO` bị missing ở đơn hàng nào thì nhiều khả năng đơn hàng đó chắc chắn không bị giao trễ.\n- Tỉ lệ giá trị của `SHIP MODE` bị missing nhiều hơn khi `label = 1` một tí khi `label = 0`. Điều này gợi ý rằng nếu cột `SHIP MODE` bị missing thì có khả năng tại đó đơn hàng bị giao trễ.","metadata":{}},{"cell_type":"code","source":"df_full_7_9['SHIP DECISION NO'].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:45.580304Z","iopub.execute_input":"2025-06-18T08:47:45.580632Z","iopub.status.idle":"2025-06-18T08:47:46.160259Z","shell.execute_reply.started":"2025-06-18T08:47:45.580605Z","shell.execute_reply":"2025-06-18T08:47:46.159203Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Vì tỉ lệ missing values của `SHIP DECISION NO` và `Ship Mode` trong dataset tương đối ít \n\n- Đối với `SHIP DECISION NO`, vì cột này là khóa ngoại (mã quyết định vận chuyển - với 249902 giá trị unique) nên không thể điền bằng logic hay thuật toán, nên việc điền missing values chủ quan chắc chắn sẽ gây vi phạm ràng buộc tham chiếu. Do ở trên ta đã phân tích và đưa ra giả thuyết rằng: 'nếu cột `SHIP DECISION NO` bị missing ở đơn hàng nào thì nhiều khả năng đơn hàng đó chắc chắn không bị giao trễ.' Nên ta sẽ thay các giá trị missing bằng giá trị -1, để bước model có thể học được mối quan hệ.\n\n- Đối với `Ship Mode`. Nếu chỉ loại bỏ các record chứa missing values có thể dẫn đến sai lầm vì có thể có một logic ẩn chứa bên trong, (đặc biệt là đơn hàng có thể hay bị delay bởi `Ship Mode`) do đó tương tự, ta sẽ điền giá trị `-1`, để bước model có thể học được mối quan hệ.","metadata":{}},{"cell_type":"code","source":"# Thay giá trị missing bằng -1 cho 2 cột được phân tích\ndf_full_7_9['SHIP DECISION NO'] = df_full_7_9['SHIP DECISION NO'].fillna(-1)\ndf_full_7_9['Ship Mode'] = df_full_7_9['Ship Mode'].fillna(-1)\n\n# In thông báo xác nhận\nprint(\"✅ Đã thay thế missing values trong 'SHIP DECISION NO' và 'Ship Mode' bằng -1.\")","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:46.161250Z","iopub.execute_input":"2025-06-18T08:47:46.161562Z","iopub.status.idle":"2025-06-18T08:47:46.376436Z","shell.execute_reply.started":"2025-06-18T08:47:46.161535Z","shell.execute_reply":"2025-06-18T08:47:46.374927Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_7_9.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_7_9)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:46.377417Z","iopub.execute_input":"2025-06-18T08:47:46.377792Z","iopub.status.idle":"2025-06-18T08:47:47.096768Z","shell.execute_reply.started":"2025-06-18T08:47:46.377762Z","shell.execute_reply":"2025-06-18T08:47:47.095843Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý OTHER AREA SHIP DIV","metadata":{}},{"cell_type":"code","source":"grouped = df_full_7_9.groupby(['OTHER AREA SHIP DIV', 'label']).size().unstack(fill_value=0)\n\n# Đảm bảo đúng thứ tự label [0, 1]\nfor lbl in [0, 1]:\n    if lbl not in grouped.columns:\n        grouped[lbl] = 0\ngrouped = grouped[[0, 1]]\n\n# Bước 2: Vẽ stacked bar chart\nx = grouped.index.astype(str)\nplt.figure(figsize=(14, 6))\nbar0 = plt.bar(x, grouped[0], label='Không trễ (label=0)', color='mediumseagreen')\nbar1 = plt.bar(x, grouped[1], bottom=grouped[0], label='Trễ (label=1)', color='tomato')\n\n# Gắn số lượng lên đỉnh cột\nfor i, name in enumerate(x):\n    total = grouped.iloc[i, 0] + grouped.iloc[i, 1]\n    plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# Cài đặt biểu đồ\nn_unique = df_full_7_9['OTHER AREA SHIP DIV'].nunique()\nplt.title(f'📦 Phân phối đơn hàng theo OTHER AREA SHIP DIV (Unique: {n_unique})', fontsize=14)\nplt.xlabel('OTHER AREA SHIP DIV')\nplt.ylabel('Số đơn hàng')\nplt.xticks(rotation=45, ha='right')\nplt.legend()\nplt.grid(axis='y', linestyle='--', alpha=0.6)\nplt.tight_layout()\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:47.098223Z","iopub.execute_input":"2025-06-18T08:47:47.098542Z","iopub.status.idle":"2025-06-18T08:47:47.402083Z","shell.execute_reply.started":"2025-06-18T08:47:47.098511Z","shell.execute_reply":"2025-06-18T08:47:47.401139Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy giá trị của `OTHER AREA SHIP DIV` chỉ có 1 giá trị unique nên ta sẽ drop","metadata":{}},{"cell_type":"code","source":"df_full_7_9.drop(columns=['OTHER AREA SHIP DIV'], inplace=True)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:47.403274Z","iopub.execute_input":"2025-06-18T08:47:47.403537Z","iopub.status.idle":"2025-06-18T08:47:47.646302Z","shell.execute_reply.started":"2025-06-18T08:47:47.403517Z","shell.execute_reply":"2025-06-18T08:47:47.645336Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý SUPPLIER_DIV","metadata":{}},{"cell_type":"code","source":"# Missing quá ít nên ta đơn giản là drop các dòng bị missing.\n# Drop các dòng bị thiếu ở SUPPLIER_DIV\ndf_full_7_9 = df_full_7_9.dropna(subset=['SUPPLIER_DIV'])","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:47.647288Z","iopub.execute_input":"2025-06-18T08:47:47.647622Z","iopub.status.idle":"2025-06-18T08:47:48.017607Z","shell.execute_reply.started":"2025-06-18T08:47:47.647592Z","shell.execute_reply":"2025-06-18T08:47:48.016712Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_full_7_9.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_full_7_9)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:48.018524Z","iopub.execute_input":"2025-06-18T08:47:48.018793Z","iopub.status.idle":"2025-06-18T08:47:48.721630Z","shell.execute_reply.started":"2025-06-18T08:47:48.018771Z","shell.execute_reply":"2025-06-18T08:47:48.720827Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Đã xong missing value 🥹🥹","metadata":{}},{"cell_type":"markdown","source":"## 🔁 **3.2 Xử lý duplicate**","metadata":{}},{"cell_type":"code","source":"df_full_7_9.duplicated().sum()","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:48.722774Z","iopub.execute_input":"2025-06-18T08:47:48.723062Z","iopub.status.idle":"2025-06-18T08:47:50.947387Z","shell.execute_reply.started":"2025-06-18T08:47:48.723032Z","shell.execute_reply":"2025-06-18T08:47:50.946567Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##  🛠️ **3.3 Data Type Checking**","metadata":{}},{"cell_type":"code","source":"# Liệt kê tất cả các dtype của các cột trong dataframe\nprint(df_full_7_9.dtypes)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:50.948189Z","iopub.execute_input":"2025-06-18T08:47:50.948442Z","iopub.status.idle":"2025-06-18T08:47:50.955049Z","shell.execute_reply.started":"2025-06-18T08:47:50.948423Z","shell.execute_reply":"2025-06-18T08:47:50.954083Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🥈 3.3 SLIVER DATA","metadata":{}},{"cell_type":"markdown","source":"### Tạo file `sliver data` để lưu dữ liệu đã qua xử lý làm sạch","metadata":{}},{"cell_type":"code","source":"for col in df_full_7_9.select_dtypes(include='object').columns:\n    df_full_7_9[col] = df_full_7_9[col].astype(str)\n    \ndf_full_7_9.to_parquet('/kaggle/working/Data for practice/df_full_7_9_sliver.parquet', index=False)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:50.956018Z","iopub.execute_input":"2025-06-18T08:47:50.956361Z","iopub.status.idle":"2025-06-18T08:47:56.712308Z","shell.execute_reply.started":"2025-06-18T08:47:50.956332Z","shell.execute_reply":"2025-06-18T08:47:56.711422Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Processing Tháng 10","metadata":{}},{"cell_type":"code","source":"# load các thư viện\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom scipy.stats import entropy, mode\nfrom sklearn.preprocessing import LabelEncoder\nfrom datetime import time","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:56.713536Z","iopub.execute_input":"2025-06-18T08:47:56.713823Z","iopub.status.idle":"2025-06-18T08:47:56.719642Z","shell.execute_reply.started":"2025-06-18T08:47:56.713798Z","shell.execute_reply":"2025-06-18T08:47:56.718650Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Dữ liệu đã cho gồm có 2 bảng định dạng CSV và EXCEL\n\n- File `PILOT_10.csv` lưu các thông tin về các đơn hàng bị delay và KHÔNG bị delay trong tháng 10.\n\n- File `Sample codebook of Delay Prediction task.xlsx` lưu thông tin ý nghĩa các trường trong dataset. Sau đây gọi là codebook.\n\nTuy nhiên có 1 số điểm chưa được đồng bộ giữa dataset và codebook (codebook chưa mô tả đủ các trường), dễ gây hiểu lầm trong quá trình làm việc.","metadata":{}},{"cell_type":"markdown","source":"# 1. **Ingestion & Metadata Logging & BRONZE Layer Data**","metadata":{}},{"cell_type":"markdown","source":"## **1.1 Data Ingestion**","metadata":{}},{"cell_type":"code","source":"# Load data\nbase_path = \"/kaggle/input/ds-108-p-21-assigment-06\"\n\n# Đọc từng file CSV vào DataFrame\ndf_10 = pd.read_csv(f\"{base_path}/PILOT_10.csv\", encoding='utf-8')\n\nprint(df_10.shape)","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:47:56.720611Z","iopub.execute_input":"2025-06-18T08:47:56.721108Z","iopub.status.idle":"2025-06-18T08:47:57.552542Z","shell.execute_reply.started":"2025-06-18T08:47:56.721074Z","shell.execute_reply":"2025-06-18T08:47:57.551581Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load codebook\ncodebook = pd.read_excel(\n    \"/kaggle/input/m-company-delay-prediction/Sample codebook of Delay Prediction task.xlsx\",\n    sheet_name=\"Columns Details\",\n    header=0,\n    index_col=0\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.553632Z","iopub.execute_input":"2025-06-18T08:47:57.553996Z","iopub.status.idle":"2025-06-18T08:47:57.584722Z","shell.execute_reply.started":"2025-06-18T08:47:57.553967Z","shell.execute_reply":"2025-06-18T08:47:57.583811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"số thuộc tính được liệt kê trong codebook: {codebook.shape[0]}\")\nprint(f\"số thuộc tính của df_10: {df_10.shape[1]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.585796Z","iopub.execute_input":"2025-06-18T08:47:57.586167Z","iopub.status.idle":"2025-06-18T08:47:57.591350Z","shell.execute_reply.started":"2025-06-18T08:47:57.586138Z","shell.execute_reply":"2025-06-18T08:47:57.590311Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"> Có thể suy luận rằng có đủ thuộc tính được định nghĩa trong codebook. Tuy nhiên nếu khẳng định vừa rồi sẽ sai nếu:\n- 1 số thuộc tính trong codebook không hề có trong df_10 => có 1 số thuộc tính không được định nghĩa\n- 1 số thuộc tính trong codebook có thể định nghĩa cho nhiều trường trong df_10 => có ít hơn thuộc tính không được định nghĩa\n\n**Phần `c. Kiểm soát schema đầu vào (tên cột, kiểu dữ liệu)` bên dưới sẽ kiểm tra vấn đề này**","metadata":{}},{"cell_type":"markdown","source":"### **a. Timestamp ingest**","metadata":{}},{"cell_type":"code","source":"from datetime import datetime\n\n# Ghi lại thời điểm ingest hiện tại\ningest_timestamp = datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\nprint(\"Ingest timestamp:\", ingest_timestamp)\n\n# Option: gắn timestamp vào toàn bộ DataFrame\ncodebook['ingest_timestamp'] = ingest_timestamp\ndf_10['ingest_timestamp'] = ingest_timestamp","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.592199Z","iopub.execute_input":"2025-06-18T08:47:57.592542Z","iopub.status.idle":"2025-06-18T08:47:57.613177Z","shell.execute_reply.started":"2025-06-18T08:47:57.592513Z","shell.execute_reply":"2025-06-18T08:47:57.612134Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **b. Data version**","metadata":{}},{"cell_type":"code","source":"# Version theo thời gian ingest\ndata_version = \"v_\" + datetime.now().strftime(\"%Y_%m_%d_%H_%M\")\nprint(\"Data version:\", data_version)\n\n# Có thể thêm vào metadata hoặc tên file lưu\ncodebook['data_version'] = data_version\ndf_10['data_version'] = data_version","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.614515Z","iopub.execute_input":"2025-06-18T08:47:57.615469Z","iopub.status.idle":"2025-06-18T08:47:57.632889Z","shell.execute_reply.started":"2025-06-18T08:47:57.615434Z","shell.execute_reply":"2025-06-18T08:47:57.632001Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧾 **1.2 Metadata Logging**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra mối quan hệ dữ liệu (`df_10` và `codebook`)\n","metadata":{}},{"cell_type":"markdown","source":"\nTrước khi phân tích dữ liệu, việc đảm bảo rằng **tất cả biến (columns) trong `df_10` có được định nghĩa trong `codebook`** là rất quan trọng.  \n\n- **`codebook`**: Chứa danh sách các biến có ý nghĩa, đóng vai trò như một tài liệu tham chiếu về cấu trúc dữ liệu (metadata).  \n- **`df_10`**: Chứa dữ liệu thực tế được thu thập, tất cả các cột phải được định nghĩa trong `codebook` để đảm bảo tính đúng đắn, khách quan khi phân tích.  \n\nSau đây tiến hành kiểm tra xem có biến nào trong `df_10` nhưng bị thiếu trong `codebook` không? Điều này giúp:  \n✔️ Đảm bảo dữ liệu không bị thiếu sót trước khi phân tích.  \n✔️ Xác định các cột không cần thiết hoặc đã đặt sai tên.  \n✔️ Chuẩn hóa dữ liệu để đảm bảo tính nhất quán.  \n\n\n---","metadata":{}},{"cell_type":"code","source":"def check_variables_in_information(codebook, df_10):\n    \"\"\"\n    Kiểm tra trong df_10 có bao nhiêu biến được định nghĩa trong codebook,\n    và bao nhiêu biến không được định nghĩa (unexpected), bỏ qua hai cột kỹ thuật.\n\n    Args:\n    - codebook: DataFrame chứa danh sách các biến hợp lệ trong cột 'Attributions'\n    - df_10: DataFrame chứa dữ liệu thực tế\n\n    Returns:\n    - None (chỉ in ra thông tin chi tiết)\n    \"\"\"\n    # Tập biến hợp lệ đã định nghĩa\n    defined_variables = set(codebook[\"Attributions\"].str.strip())\n\n    # Các cột cần bỏ qua trong quá trình kiểm tra\n    ignored_columns = {\"data_version\", \"ingest_timestamp\"}\n\n    # Tập biến thực tế trong dữ liệu (sau khi loại bỏ các cột bị bỏ qua)\n    info_variables = set(df_10.columns.str.strip()) - ignored_columns\n\n    # Biến có trong df_10 mà cũng được định nghĩa\n    matched_variables = info_variables & defined_variables\n\n    # Biến có trong df_10 nhưng KHÔNG được định nghĩa\n    unexpected_variables = info_variables - defined_variables\n\n    print(f\"Tổng số biến trong df_10 (bỏ qua cột kỹ thuật): {len(info_variables)}\")\n    print(f\"Số biến được định nghĩa trong codebook: {len(matched_variables)}\")\n    print(f\"Số biến KHÔNG được định nghĩa trong codebook: {len(unexpected_variables)}\")\n\n    print(\"\\nDanh sách biến ĐƯỢC định nghĩa:\")\n    print(sorted(matched_variables))\n\n    if unexpected_variables:\n        print(\"\\nDanh sách biến KHÔNG ĐƯỢC định nghĩa:\")\n        print(sorted(unexpected_variables))\n    else:\n        print(\"\\nKhông có biến nào bất ngờ! Mọi thứ đều đúng chuẩn định nghĩa.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.634161Z","iopub.execute_input":"2025-06-18T08:47:57.634460Z","iopub.status.idle":"2025-06-18T08:47:57.650702Z","shell.execute_reply.started":"2025-06-18T08:47:57.634439Z","shell.execute_reply":"2025-06-18T08:47:57.649934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"check_variables_in_information(codebook, df_10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.651703Z","iopub.execute_input":"2025-06-18T08:47:57.651996Z","iopub.status.idle":"2025-06-18T08:47:57.790682Z","shell.execute_reply.started":"2025-06-18T08:47:57.651969Z","shell.execute_reply":"2025-06-18T08:47:57.789755Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **2. Data Validation & Profiling**","metadata":{}},{"cell_type":"markdown","source":"## **🧭 2.1 Data Profiling**","metadata":{}},{"cell_type":"markdown","source":"### a. Kiểm tra kiểu dữ liệu","metadata":{}},{"cell_type":"code","source":"# In ra tên cột và dtype tương ứng trong DataFrame df_10\ncodebook[['Attributions', 'Data Type']].head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.791682Z","iopub.execute_input":"2025-06-18T08:47:57.792027Z","iopub.status.idle":"2025-06-18T08:47:57.816117Z","shell.execute_reply.started":"2025-06-18T08:47:57.791996Z","shell.execute_reply":"2025-06-18T08:47:57.814953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cập nhật giá trị trong cột 'Data Type' nếu Attributions là order date hoặc 'VSD'\ncodebook.loc[codebook['Attributions'] == 'VSD', 'Data Type'] = 'DateTime'\ncodebook.loc[codebook['Attributions'] == 'Order date', 'Data Type'] = 'DateTime'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.817679Z","iopub.execute_input":"2025-06-18T08:47:57.818000Z","iopub.status.idle":"2025-06-18T08:47:57.836961Z","shell.execute_reply.started":"2025-06-18T08:47:57.817969Z","shell.execute_reply":"2025-06-18T08:47:57.836198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def convert_column_dtypes_from_codebook(df: pd.DataFrame, codebook: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"\n    Chuyển đổi kiểu dữ liệu của các cột trong DataFrame đầu vào dựa trên định nghĩa từ codebook.\n\n    Args:\n        df (pd.DataFrame): DataFrame chứa dữ liệu gốc cần chuyển kiểu dữ liệu (vd: df_10).\n        codebook (pd.DataFrame): DataFrame định nghĩa kiểu dữ liệu cho từng cột, \n            yêu cầu có các cột:\n                - 'Attributions': tên cột trong df\n                - 'Data Type': kiểu dữ liệu đích (int64, float, object, DateTime...)\n\n    Returns:\n        pd.DataFrame: DataFrame đầu vào sau khi đã cố gắng chuyển đổi kiểu dữ liệu các cột tương ứng.\n\n    In ra:\n        - ✅ Với mỗi cột đổi thành công → có thể in ra tên cột và kiểu đã chuyển\n        - ❌ Với mỗi cột lỗi → in ra tên cột, kiểu định chuyển, và lý do lỗi (exception message)\n\n    Notes:\n        - Các kiểu dữ liệu hỗ trợ gồm: int, float, object/string, DateTime\n        - Với kiểu int sử dụng kiểu nullable 'Int64' của pandas để giữ lại giá trị NaN nếu có\n        - Nếu kiểu dữ liệu không được nhận diện hoặc lỗi ép kiểu → cột sẽ bị bỏ qua\n\n    Ví dụ dùng:\n        df_10 = convert_column_dtypes_from_codebook(df_10, codebook_full)\n    \"\"\"\n    for _, row in codebook.iterrows():\n        col = row['Attributions']\n        dtype = row['Data Type']\n\n        try:\n            if dtype.lower() in ['int', 'int64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('Int64')\n            elif dtype.lower() in ['float', 'float64']:\n                df[col] = pd.to_numeric(df[col], errors='raise').astype('float')\n            elif dtype.lower() in ['object', 'string']:\n                df[col] = df[col].astype(str)\n            elif dtype.lower() in ['datetime', 'datetime64', 'datetime64[ns]']:\n                try:\n                    # Cố gắng parse kiểu ISO8601 trước\n                    df[col] = pd.to_datetime(df[col], format='ISO8601', errors='raise')\n                    print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (ISO8601)\")\n                except Exception as e_iso:\n                    try:\n                        # Cố gắng với format kiểu Excel: \"4/1/2022  12:00:00 AM\"\n                        df[col] = pd.to_datetime(df[col], format=\"%m/%d/%Y %I:%M:%S %p\", errors='raise')\n                        print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (Excel-style)\")\n                    except Exception as e_excel:\n                        try:\n                            # Cuối cùng là mixed\n                            df[col] = pd.to_datetime(df[col], format='mixed', errors='raise')\n                            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → DateTime (mixed format)\")\n                        except Exception as e_mixed:\n                            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → DateTime | ISO8601: {e_iso} | Excel: {e_excel} | mixed: {e_mixed}\")\n\n            else:\n                print(f\"Không rõ kiểu dữ liệu '{dtype}' cho cột '{col}', bỏ qua.\")\n                continue\n\n            print(f\"Đã đổi kiểu dữ liệu thành công cho cột: {col} → {dtype}\")\n        \n        except Exception as e:\n            print(f\"Lỗi khi đổi kiểu dữ liệu cho cột: {col} → {dtype} | Lý do: {e}\")\n\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.838112Z","iopub.execute_input":"2025-06-18T08:47:57.838439Z","iopub.status.idle":"2025-06-18T08:47:57.863244Z","shell.execute_reply.started":"2025-06-18T08:47:57.838413Z","shell.execute_reply":"2025-06-18T08:47:57.862224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10 = convert_column_dtypes_from_codebook(df_10, codebook)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:57.864237Z","iopub.execute_input":"2025-06-18T08:47:57.864502Z","iopub.status.idle":"2025-06-18T08:47:58.071050Z","shell.execute_reply.started":"2025-06-18T08:47:57.864482Z","shell.execute_reply":"2025-06-18T08:47:58.070116Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Phân rã thuộc tính kiểu datetime, time**","metadata":{}},{"cell_type":"code","source":"# Tách Order date\ndf_10['order_year'] = df_10['Order date'].dt.year\ndf_10['order_month'] = df_10['Order date'].dt.month\ndf_10['order_dayofweek'] = df_10['Order date'].dt.dayofweek\ndf_10['is_order_weekend'] = df_10['order_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tách VSD\ndf_10['vsd_year'] = df_10['VSD'].dt.year\ndf_10['vsd_month'] = df_10['VSD'].dt.month\ndf_10['vsd_dayofweek'] = df_10['VSD'].dt.dayofweek\ndf_10['is_vsd_weekend'] = df_10['vsd_dayofweek'].apply(lambda x: 1 if x >= 5 else 0)\n\n# Tính khoảng cách giữa VSD và Order date (nếu hợp lý về logic nghiệp vụ)\ndf_10['days_to_vsd'] = (df_10['VSD'] - df_10['Order date']).dt.days","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.072061Z","iopub.execute_input":"2025-06-18T08:47:58.072376Z","iopub.status.idle":"2025-06-18T08:47:58.205670Z","shell.execute_reply.started":"2025-06-18T08:47:58.072354Z","shell.execute_reply":"2025-06-18T08:47:58.204687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10['vsd_year'].value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.206999Z","iopub.execute_input":"2025-06-18T08:47:58.207272Z","iopub.status.idle":"2025-06-18T08:47:58.216025Z","shell.execute_reply.started":"2025-06-18T08:47:58.207250Z","shell.execute_reply":"2025-06-18T08:47:58.215164Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"`REASON_CD` → float64 \n>Ý nghĩa cột: Mã lý do đơn hàng bị delay.\n\n>Nhận định: Những dòng có chứa giá trị thường ứng với label = 1, tức đơn hàng bị giao trễ. Điều này cho thấy REASON_CD là một thông tin chỉ xuất hiện sau khi đơn hàng đã bị delay, hay nói cách khác là dữ liệu thuộc tương lai so với thời điểm ta cần dự đoán.\n\n>Hành động: Vì không thể sử dụng dữ liệu tương lai trong quá trình huấn luyện mô hình (training), nên cột REASON_CD sẽ bị loại bỏ khỏi dataset.","metadata":{}},{"cell_type":"code","source":"print(df_10['REASON_CD'].unique()) ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.216936Z","iopub.execute_input":"2025-06-18T08:47:58.217259Z","iopub.status.idle":"2025-06-18T08:47:58.237695Z","shell.execute_reply.started":"2025-06-18T08:47:58.217233Z","shell.execute_reply":"2025-06-18T08:47:58.236708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tuy nhiên vì cột này là dữ liệu tương lai, ta sẽ loại bỏ:\ndf_10 = df_10.drop(columns=['REASON_CD'])  # loại bỏ hoàn toàn cột REASON_CD\ncodebook = codebook[codebook['Attributions'] != 'REASON_CD']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.238729Z","iopub.execute_input":"2025-06-18T08:47:58.239085Z","iopub.status.idle":"2025-06-18T08:47:58.288725Z","shell.execute_reply.started":"2025-06-18T08:47:58.239056Z","shell.execute_reply":"2025-06-18T08:47:58.287659Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"> Ta chuyển kiểu dữ liệu 1 số trường là khóa ngoại về `object`","metadata":{}},{"cell_type":"code","source":"cols_to_convert = ['CLASSIFY_CD', 'CUST_CD', 'SHIP DECISION NO']\ndf_10[cols_to_convert] = df_10[cols_to_convert].astype('object')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.289698Z","iopub.execute_input":"2025-06-18T08:47:58.290208Z","iopub.status.idle":"2025-06-18T08:47:58.319026Z","shell.execute_reply.started":"2025-06-18T08:47:58.290171Z","shell.execute_reply":"2025-06-18T08:47:58.318142Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic của 1 số cột về missing values","metadata":{}},{"cell_type":"code","source":"df_10.isnull().sum().sort_values(ascending=False)[:7]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.319818Z","iopub.execute_input":"2025-06-18T08:47:58.320108Z","iopub.status.idle":"2025-06-18T08:47:58.433619Z","shell.execute_reply.started":"2025-06-18T08:47:58.320086Z","shell.execute_reply":"2025-06-18T08:47:58.432795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10['QTUF_RCV_NO'].value_counts().sort_values(ascending=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.434559Z","iopub.execute_input":"2025-06-18T08:47:58.434787Z","iopub.status.idle":"2025-06-18T08:47:58.443343Z","shell.execute_reply.started":"2025-06-18T08:47:58.434770Z","shell.execute_reply":"2025-06-18T08:47:58.442497Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10['SOUF_RCV_NO'].value_counts().sort_values(ascending=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.444242Z","iopub.execute_input":"2025-06-18T08:47:58.444564Z","iopub.status.idle":"2025-06-18T08:47:58.480418Z","shell.execute_reply.started":"2025-06-18T08:47:58.444537Z","shell.execute_reply":"2025-06-18T08:47:58.479205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10['Ship Mode'].value_counts().sort_values(ascending=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.481625Z","iopub.execute_input":"2025-06-18T08:47:58.481974Z","iopub.status.idle":"2025-06-18T08:47:58.519463Z","shell.execute_reply.started":"2025-06-18T08:47:58.481945Z","shell.execute_reply":"2025-06-18T08:47:58.518359Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta thấy cột `SOUF_RCV_NO` và `Ship Mode` mặc dù đa phần giá trị là `nan` nhưng do cột đang kiểu object nên bị hiểu lầm là chuỗi string. Nên ta sẽ chuyển nó về kiểu float và thay giá trị chuỗi `nan` thành `NAN`","metadata":{}},{"cell_type":"code","source":"# Thay các chuỗi 'nan' (chuỗi chữ, không phải NaN thật sự) thành np.nan\ndf_10['SOUF_RCV_NO'] = df_10['SOUF_RCV_NO'].replace('nan', np.nan)\ndf_10['Ship Mode'] = df_10['Ship Mode'].replace('nan', np.nan)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.521671Z","iopub.execute_input":"2025-06-18T08:47:58.522130Z","iopub.status.idle":"2025-06-18T08:47:58.577997Z","shell.execute_reply.started":"2025-06-18T08:47:58.522103Z","shell.execute_reply":"2025-06-18T08:47:58.576712Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra logic ẩn của 1 số cột","metadata":{}},{"cell_type":"markdown","source":"🔧 Vấn đề: Cột SO_TIME (Giờ đặt hàng) hiện đang được mã hóa sai kiểu dữ liệu – dưới dạng số nguyên (int), `ví dụ: 115325 → thực chất là 11:53:25`\n\n\nTa cần chuyển đổi các giá trị số này về kiểu thời gian chuẩn (datetime.time) để xử lý chính xác hơn ở các bước phân tích và trích xuất đặc trưng.","metadata":{}},{"cell_type":"code","source":"def convert_to_time(x):\n    try:\n        x_str = str(int(x)).zfill(6)\n        hour = int(x_str[:2])\n        minute = int(x_str[2:4])\n        second = int(x_str[4:6])\n        return time(hour, minute, second)\n    except:\n        return pd.NaT\n\n# Áp dụng\ndf_10['SO_TIME'] = df_10['SO_TIME'].apply(convert_to_time)\n\n\n# Kiểm tra kết quả\nprint(\"✅ Đã chuẩn hóa cột 'SO_TIME' về kiểu thời gian thực tế.\")\ndf_10['SO_TIME'].head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.578959Z","iopub.execute_input":"2025-06-18T08:47:58.579638Z","iopub.status.idle":"2025-06-18T08:47:58.745088Z","shell.execute_reply.started":"2025-06-18T08:47:58.579589Z","shell.execute_reply":"2025-06-18T08:47:58.744239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Đảm bảo cột SO_TIME đã là kiểu datetime.time\n# Nếu chưa: df_10['SO_TIME'] = df_10['SO_TIME'].apply(convert_to_time)\n\n# Tạo đặc trưng từ SO_TIME\ndf_10['so_hour'] = df_10['SO_TIME'].apply(lambda x: x.hour if pd.notnull(x) else np.nan)\ndf_10['so_minute'] = df_10['SO_TIME'].apply(lambda x: x.minute if pd.notnull(x) else np.nan)\ndf_10['so_second'] = df_10['SO_TIME'].apply(lambda x: x.second if pd.notnull(x) else np.nan)\n\n# Buổi sáng hay chiều\ndf_10['is_morning'] = df_10['so_hour'].apply(lambda x: 1 if x < 12 else 0 if pd.notnull(x) else np.nan)\n\n# Gán time slot theo khung giờ sinh hoạt\ndef assign_time_slot(hour):\n    if pd.isnull(hour): return np.nan\n    elif 0 <= hour < 6: return 'midnight'\n    elif 6 <= hour < 12: return 'morning'\n    elif 12 <= hour < 18: return 'afternoon'\n    else: return 'evening'\n\ndf_10['time_slot'] = df_10['so_hour'].apply(assign_time_slot)\n\n# Binned theo khung 4 tiếng\ndf_10['time_bin'] = pd.cut(df_10['so_hour'],\n                                 bins=[-1, 3, 7, 11, 15, 19, 23],\n                                 labels=['0-3h', '4-7h', '8-11h', '12-15h', '16-19h', '20-23h'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:58.746139Z","iopub.execute_input":"2025-06-18T08:47:58.746426Z","iopub.status.idle":"2025-06-18T08:47:59.162910Z","shell.execute_reply.started":"2025-06-18T08:47:58.746399Z","shell.execute_reply":"2025-06-18T08:47:59.161692Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### b. Thông tin missing và duplicate trên toàn bộ tập dữ liệu","metadata":{}},{"cell_type":"code","source":"def print_data_overview(df, columns=None):\n    if columns is not None:\n        df = df[columns]\n\n    num_rows = df.shape[0]\n    num_cols = df.shape[1]\n    total_cells = num_rows * num_cols\n    percent_missing = df.isnull().sum().sum() / total_cells * 100\n    percent_duplicate = df.duplicated().sum() / num_rows * 100\n\n    print(f\"Tổng quan: {num_rows} dòng, {num_cols} cột\")\n    print(f\"⚠️  {percent_missing:.4f}% missing | 🔁 {percent_duplicate:.4f}% duplicate\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.163803Z","iopub.execute_input":"2025-06-18T08:47:59.164107Z","iopub.status.idle":"2025-06-18T08:47:59.170137Z","shell.execute_reply.started":"2025-06-18T08:47:59.164077Z","shell.execute_reply":"2025-06-18T08:47:59.168918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.171038Z","iopub.execute_input":"2025-06-18T08:47:59.171289Z","iopub.status.idle":"2025-06-18T08:47:59.574129Z","shell.execute_reply.started":"2025-06-18T08:47:59.171269Z","shell.execute_reply":"2025-06-18T08:47:59.573279Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### c. Các giá trị thống kê, đo lường độ Trung tâm và Phân Tán của dữ liệu","metadata":{}},{"cell_type":"markdown","source":">**Danh sách các cột mang ý nghĩa thống kê (Không bao gồm khóa ngoại, khóa chính, code), lưu ý kiểu dữ liệu của một số cột đã được xử lý thủ công**","metadata":{}},{"cell_type":"code","source":"print(df_10.select_dtypes(exclude='object').columns)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.575143Z","iopub.execute_input":"2025-06-18T08:47:59.575449Z","iopub.status.idle":"2025-06-18T08:47:59.602035Z","shell.execute_reply.started":"2025-06-18T08:47:59.575420Z","shell.execute_reply":"2025-06-18T08:47:59.600870Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.603028Z","iopub.execute_input":"2025-06-18T08:47:59.603293Z","iopub.status.idle":"2025-06-18T08:47:59.854466Z","shell.execute_reply.started":"2025-06-18T08:47:59.603272Z","shell.execute_reply":"2025-06-18T08:47:59.853546Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧪 **2.2 Data Validation – Kiểm tra chất lượng & chuẩn đầu vào**","metadata":{}},{"cell_type":"code","source":"df_10.isnull().sum().sort_values(ascending=False)[:7]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.855425Z","iopub.execute_input":"2025-06-18T08:47:59.855742Z","iopub.status.idle":"2025-06-18T08:47:59.978592Z","shell.execute_reply.started":"2025-06-18T08:47:59.855716Z","shell.execute_reply":"2025-06-18T08:47:59.977587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10.duplicated().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:47:59.979413Z","iopub.execute_input":"2025-06-18T08:47:59.979718Z","iopub.status.idle":"2025-06-18T08:48:00.243342Z","shell.execute_reply.started":"2025-06-18T08:47:59.979697Z","shell.execute_reply":"2025-06-18T08:48:00.242426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_10.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_10)) * 100\n\n# Sắp xếp và in ra top 7 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(7)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:00.244676Z","iopub.execute_input":"2025-06-18T08:48:00.244952Z","iopub.status.idle":"2025-06-18T08:48:00.369870Z","shell.execute_reply.started":"2025-06-18T08:48:00.244931Z","shell.execute_reply":"2025-06-18T08:48:00.369032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tổng quan dữ liệu\nprint_data_overview(df_10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:00.370755Z","iopub.execute_input":"2025-06-18T08:48:00.371069Z","iopub.status.idle":"2025-06-18T08:48:00.740512Z","shell.execute_reply.started":"2025-06-18T08:48:00.371044Z","shell.execute_reply":"2025-06-18T08:48:00.739538Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### Kiểm tra các trường có ít giá trị unique - các trường chỉ toàn là 1 giá trị","metadata":{}},{"cell_type":"code","source":"def get_highly_dominant_columns(df, threshold=0.99):\n    \"\"\"\n    Trả về danh sách các cột mà 1 giá trị chiếm ≥ threshold (default 0.99 = 99%)\n    \"\"\"\n    dominant_cols = []\n\n    for col in df.columns:\n        value_counts = df[col].value_counts(normalize=True, dropna=False)\n        if not value_counts.empty:\n            top_freq = value_counts.values[0]\n            if top_freq >= threshold:\n                dominant_val = value_counts.idxmax()\n                dominant_cols.append((col, top_freq, dominant_val))\n\n    return dominant_cols","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:00.741464Z","iopub.execute_input":"2025-06-18T08:48:00.741740Z","iopub.status.idle":"2025-06-18T08:48:00.747449Z","shell.execute_reply.started":"2025-06-18T08:48:00.741719Z","shell.execute_reply":"2025-06-18T08:48:00.746370Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dominant_columns_info = get_highly_dominant_columns(df_10, threshold=0.99)\n\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:00.748329Z","iopub.execute_input":"2025-06-18T08:48:00.748632Z","iopub.status.idle":"2025-06-18T08:48:01.075265Z","shell.execute_reply.started":"2025-06-18T08:48:00.748604Z","shell.execute_reply":"2025-06-18T08:48:01.073890Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Tìm **những cột mà trong đó có một giá trị chiếm >99%**, tức là mất cân bằng dữ liệu cực kỳ cao → có thể không hữu ích cho mô hình hóa hoặc gây nhiễu/overfit, nên ở bước sau ta sẽ xóa.","metadata":{}},{"cell_type":"markdown","source":"# 3. DATA CLEANING & REPAIRING & SLIVER Layer Data","metadata":{}},{"cell_type":"markdown","source":"## 🫳**3.0 Drop meaningless column**","metadata":{}},{"cell_type":"code","source":"# Gọi hàm\ndominant_columns_info = get_highly_dominant_columns(df_10, threshold=0.99)\n\n# In danh sách trước khi xoá\nprint(\"📌 Các cột sẽ bị drop (1 giá trị chiếm ≥99%):\")\nfor col, freq, val in dominant_columns_info:\n    print(f\"🧩 {col}: '{val}' chiếm {freq:.2%}\")\n\n# Thực hiện drop\ncolumns_to_drop = [col for col, _, _ in dominant_columns_info]\ndf_10 = df_10.drop(columns=columns_to_drop)\n\nprint(f\"\\n✅ Đã xoá {len(columns_to_drop)} cột. DataFrame giờ còn {df_10.shape[1]} cột.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.076258Z","iopub.execute_input":"2025-06-18T08:48:01.076523Z","iopub.status.idle":"2025-06-18T08:48:01.401033Z","shell.execute_reply.started":"2025-06-18T08:48:01.076503Z","shell.execute_reply":"2025-06-18T08:48:01.400242Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧽 **3.1 Detecting And Handling Missing Values**","metadata":{}},{"cell_type":"code","source":"missing_info = df_10.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_10)) * 100\n\nmissing_info.sort_values(by='missing_count', ascending=False).head(7)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.402233Z","iopub.execute_input":"2025-06-18T08:48:01.402544Z","iopub.status.idle":"2025-06-18T08:48:01.507884Z","shell.execute_reply.started":"2025-06-18T08:48:01.402518Z","shell.execute_reply":"2025-06-18T08:48:01.506974Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SOUF_RCV_NO`","metadata":{}},{"cell_type":"markdown","source":"Ta nhận thấy trường `SOUF_RCV_NO - Số tiếp tân không hợp lệ tại thời điểm đặt hàng` bị missing nhiều nhất - 96.3%, đây là dấu hiệu `MNAR - Missing Not At Random - Missing do nội tại của cột đó`. Việc điền missing value bằng các kỹ thuật là điều không thể do logic không rõ ràng và thiếu phần lớn dữ liệu, nên ta sẽ bỏ cột trên.","metadata":{}},{"cell_type":"code","source":"df_10.drop(columns=['SOUF_RCV_NO'], inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.508817Z","iopub.execute_input":"2025-06-18T08:48:01.509173Z","iopub.status.idle":"2025-06-18T08:48:01.547742Z","shell.execute_reply.started":"2025-06-18T08:48:01.509143Z","shell.execute_reply":"2025-06-18T08:48:01.546795Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `OTHER AREA SHIP DIV`","metadata":{}},{"cell_type":"code","source":"df_10.drop(columns=['OTHER AREA SHIP DIV'], inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.548657Z","iopub.execute_input":"2025-06-18T08:48:01.548955Z","iopub.status.idle":"2025-06-18T08:48:01.587243Z","shell.execute_reply.started":"2025-06-18T08:48:01.548927Z","shell.execute_reply":"2025-06-18T08:48:01.586271Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_10.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_10)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.588205Z","iopub.execute_input":"2025-06-18T08:48:01.588472Z","iopub.status.idle":"2025-06-18T08:48:01.692776Z","shell.execute_reply.started":"2025-06-18T08:48:01.588452Z","shell.execute_reply":"2025-06-18T08:48:01.691321Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Xử lý `SHIP MODE` và `SUPPLIER_DIV`","metadata":{}},{"cell_type":"code","source":"# Thay giá trị missing bằng -1 cho 2 cột được phân tích\ndf_10['SUPPLIER_DIV'] = df_10['SUPPLIER_DIV'].fillna(-1)\ndf_10['Ship Mode'] = df_10['Ship Mode'].fillna(-1)\n\n# In thông báo xác nhận\nprint(\"✅ Đã thay thế missing values trong 'SUPPLIER_DIV' và 'Ship Mode' bằng -1.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.693737Z","iopub.execute_input":"2025-06-18T08:48:01.694075Z","iopub.status.idle":"2025-06-18T08:48:01.715915Z","shell.execute_reply.started":"2025-06-18T08:48:01.694045Z","shell.execute_reply":"2025-06-18T08:48:01.713892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_info = df_10.isnull().sum().to_frame(name='missing_count')\nmissing_info['missing_percent'] = (missing_info['missing_count'] / len(df_10)) * 100\n\n# Sắp xếp và in ra top 5 cột missing nhiều nhất\nmissing_info.sort_values(by='missing_count', ascending=False).head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.716671Z","iopub.execute_input":"2025-06-18T08:48:01.717026Z","iopub.status.idle":"2025-06-18T08:48:01.825349Z","shell.execute_reply.started":"2025-06-18T08:48:01.716998Z","shell.execute_reply":"2025-06-18T08:48:01.824443Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Đã xong missing value 🥹🥹","metadata":{}},{"cell_type":"markdown","source":"## 🔁 **3.2 Xử lý duplicate**","metadata":{}},{"cell_type":"code","source":"df_10.duplicated().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:01.826602Z","iopub.execute_input":"2025-06-18T08:48:01.826896Z","iopub.status.idle":"2025-06-18T08:48:02.057289Z","shell.execute_reply.started":"2025-06-18T08:48:01.826850Z","shell.execute_reply":"2025-06-18T08:48:02.056049Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"##  🛠️ **3.3 Data Type Checking**","metadata":{}},{"cell_type":"code","source":"# Liệt kê tất cả các dtype của các cột trong dataframe\nprint(df_10.dtypes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:02.058742Z","iopub.execute_input":"2025-06-18T08:48:02.059137Z","iopub.status.idle":"2025-06-18T08:48:02.065887Z","shell.execute_reply.started":"2025-06-18T08:48:02.059111Z","shell.execute_reply":"2025-06-18T08:48:02.064890Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🥈 3.3 SLIVER DATA","metadata":{}},{"cell_type":"markdown","source":"### Tạo file `sliver data` để lưu dữ liệu đã qua xử lý làm sạch","metadata":{}},{"cell_type":"code","source":"for col in df_10.select_dtypes(include='object').columns:\n    df_10[col] = df_10[col].astype(str)\n    \ndf_10.to_parquet('/kaggle/working/Data for practice/df_10_sliver.parquet', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:02.066886Z","iopub.execute_input":"2025-06-18T08:48:02.067186Z","iopub.status.idle":"2025-06-18T08:48:02.728013Z","shell.execute_reply.started":"2025-06-18T08:48:02.067163Z","shell.execute_reply":"2025-06-18T08:48:02.727077Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<!-- Tiêu đề 2: Thực nghiệm trên mô hình -->\n<div style=\"background: linear-gradient(to right, #a18cd1, #fbc2eb); padding: 16px; border-radius: 12px; text-align: center; font-size: 28px; font-weight: bold; color: #333;\">\n  🧪 Thực nghiệm trên mô hình\n</div>","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nimport pandas as pd\nimport numpy as np\nfrom sklearn.utils import resample\n\nfrom lightgbm import LGBMClassifier, early_stopping, log_evaluation\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, roc_auc_score\nfrom sklearn.metrics import f1_score\n\nimport shap\nimport optuna","metadata":{"execution":{"iopub.status.busy":"2025-06-18T08:48:02.729241Z","iopub.execute_input":"2025-06-18T08:48:02.729550Z","iopub.status.idle":"2025-06-18T08:48:17.537312Z","shell.execute_reply.started":"2025-06-18T08:48:02.729522Z","shell.execute_reply":"2025-06-18T08:48:17.536330Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nbase_data_url_folder = '/kaggle/working/Data for practice'\n\n# Đọc dữ liệu từ các file parquet\ndf_4_6 = pd.read_parquet(os.path.join(base_data_url_folder, \"df_full_4_6_sliver.parquet\"))\ndf_7_9 = pd.read_parquet(os.path.join(base_data_url_folder, \"df_full_7_9_sliver.parquet\"))\ndf_10 = pd.read_parquet(os.path.join(base_data_url_folder, \"df_10_sliver.parquet\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:17.538208Z","iopub.execute_input":"2025-06-18T08:48:17.538833Z","iopub.status.idle":"2025-06-18T08:48:20.916170Z","shell.execute_reply.started":"2025-06-18T08:48:17.538801Z","shell.execute_reply":"2025-06-18T08:48:20.915221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Giả định dữ liệu vào tháng 7 không còn phù hợp cho training và tetsing tháng 10, vì nó không còn tính mới\n# nên ta chỉ dùng tháng 8 và tháng 9\ndf_7_9 = df_7_9[df_7_9['Order date'].dt.month != 7] ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:20.917076Z","iopub.execute_input":"2025-06-18T08:48:20.917338Z","iopub.status.idle":"2025-06-18T08:48:21.261804Z","shell.execute_reply.started":"2025-06-18T08:48:20.917318Z","shell.execute_reply":"2025-06-18T08:48:21.260767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"DataFrame 4-6:\")\nprint(df_4_6.shape)\n\nprint(\"\\nDataFrame 7-9:\")\nprint(df_7_9.shape)\n\nprint(\"\\nDataFrame 10:\")\nprint(df_10.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:21.263055Z","iopub.execute_input":"2025-06-18T08:48:21.263408Z","iopub.status.idle":"2025-06-18T08:48:21.269428Z","shell.execute_reply.started":"2025-06-18T08:48:21.263377Z","shell.execute_reply":"2025-06-18T08:48:21.268424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(df_4_6.isnull().sum().sort_values(ascending=False).head())\nprint(df_7_9.isnull().sum().sort_values(ascending=False).head())\nprint(df_10.isnull().sum().sort_values(ascending=False).head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:21.270377Z","iopub.execute_input":"2025-06-18T08:48:21.270774Z","iopub.status.idle":"2025-06-18T08:48:22.210094Z","shell.execute_reply.started":"2025-06-18T08:48:21.270745Z","shell.execute_reply":"2025-06-18T08:48:22.209300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compare_delay_labels(df1, df2, df1_name='df1', df2_name='df2', label_col='label', show_table=True):\n    \"\"\"\n    So sánh số lượng label = 0 (không delay) và label = 1 (delay) giữa hai DataFrame.\n\n    Parameters:\n    - df1, df2: Hai DataFrame cần so sánh\n    - df1_name, df2_name: Tên DataFrame hiển thị trong bảng\n    - label_col: Tên cột chứa label (mặc định là 'label')\n    - show_table: Nếu True thì in bảng ra console\n\n    Returns:\n    - summary_table: DataFrame dạng 2x2 chứa kết quả thống kê\n    \"\"\"\n    # Đếm số lượng label\n    count_1 = df1[label_col].value_counts().sort_index()\n    count_2 = df2[label_col].value_counts().sort_index()\n\n    # Tạo bảng thống kê\n    summary_table = pd.DataFrame({\n        df1_name: count_1,\n        df2_name: count_2\n    }).fillna(0).astype(int)\n\n    # Đặt tên index dễ đọc\n    summary_table.index = ['Không delay (0)', 'Delay (1)']\n\n    if show_table:\n        print(f\"Bảng so sánh số lượng đơn delay và không delay giữa {df1_name} và {df2_name}:\")\n        # print(summary_table)\n\n    return summary_table","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.210972Z","iopub.execute_input":"2025-06-18T08:48:22.211275Z","iopub.status.idle":"2025-06-18T08:48:22.217543Z","shell.execute_reply.started":"2025-06-18T08:48:22.211248Z","shell.execute_reply":"2025-06-18T08:48:22.216625Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"compare_delay_labels(df_4_6, df_7_9, df1_name='df_4_6', df2_name='df_7_9')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.218437Z","iopub.execute_input":"2025-06-18T08:48:22.218679Z","iopub.status.idle":"2025-06-18T08:48:22.254758Z","shell.execute_reply.started":"2025-06-18T08:48:22.218662Z","shell.execute_reply":"2025-06-18T08:48:22.253780Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# A. Chuẩn hóa 2 bộ dữ liệu về cùng 1 định dạng","metadata":{}},{"cell_type":"markdown","source":"## 1. So sánh tên cột (columns) giữa 2 DataFrame df_7_9 và df_10 xem có gì khác biệt không","metadata":{}},{"cell_type":"code","source":"def compare_columns(df1, df2, df1_name='df1', df2_name='df2', verbose=True):\n    \"\"\"\n    So sánh các cột giữa hai DataFrame và in ra những cột chỉ có ở một trong hai.\n\n    Parameters:\n    - df1, df2: Hai DataFrame cần so sánh\n    - df1_name, df2_name: Tên hiển thị của hai DataFrame (tùy chọn)\n    - verbose: Nếu True sẽ in kết quả ra màn hình\n\n    Returns:\n    - Tuple gồm (only_in_df1, only_in_df2): hai tập hợp tên cột chỉ có ở từng DataFrame\n    \"\"\"\n    cols_df1 = set(df1.columns)\n    cols_df2 = set(df2.columns)\n\n    only_in_df1 = cols_df1 - cols_df2\n    only_in_df2 = cols_df2 - cols_df1\n\n    if verbose:\n        print(f\"Các cột chỉ có trong {df1_name}:\")\n        print(only_in_df1 if only_in_df1 else \"✅ Không có, quá là đồng bộ!\")\n\n        print(f\"\\nCác cột chỉ có trong {df2_name}:\")\n        print(only_in_df2 if only_in_df2 else \"✅ Không có, siêu khớp luôn!\")\n\n    return only_in_df1, only_in_df2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.255740Z","iopub.execute_input":"2025-06-18T08:48:22.256219Z","iopub.status.idle":"2025-06-18T08:48:22.272545Z","shell.execute_reply.started":"2025-06-18T08:48:22.256188Z","shell.execute_reply":"2025-06-18T08:48:22.271637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"only_in_10, only_in_7_9 = compare_columns(df_10, df_7_9, df1_name='df_10', df2_name='df_7_9')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.273499Z","iopub.execute_input":"2025-06-18T08:48:22.273750Z","iopub.status.idle":"2025-06-18T08:48:22.296527Z","shell.execute_reply.started":"2025-06-18T08:48:22.273730Z","shell.execute_reply":"2025-06-18T08:48:22.295576Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Ta sẽ bỏ các cột chỉ có trong `df_7_9` mà không có trong `df_10` ","metadata":{}},{"cell_type":"code","source":"# Loại bỏ các cột không có trong df_7_9 khỏi df_10, trừ cột 'ID'\ncols_to_drop_in_10 = [col for col in only_in_10 if col != 'ID']\ndf_10 = df_10.drop(columns=cols_to_drop_in_10)\nprint(\"Đã loại bỏ các cột không cần thiết khỏi df_10, giữ lại 'ID'!\")\n\n# Loại bỏ các cột không có trong df_10 khỏi df_7_9, trừ cột 'label'\ncols_to_drop_in_7_9 = [col for col in only_in_7_9 if col != 'label']\ndf_7_9 = df_7_9.drop(columns=cols_to_drop_in_7_9)\nprint(\"Đã loại bỏ các cột không cần thiết khỏi df_7_9, giữ lại 'label'!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.297622Z","iopub.execute_input":"2025-06-18T08:48:22.297983Z","iopub.status.idle":"2025-06-18T08:48:22.528236Z","shell.execute_reply.started":"2025-06-18T08:48:22.297956Z","shell.execute_reply":"2025-06-18T08:48:22.527435Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. So sánh kiểu dữ liệu của các cột giữa 2 DataFrame df_7_9 và df_10 xem có gì khác biệt không","metadata":{}},{"cell_type":"code","source":"def compare_column_dtypes(df1, df2, df1_name='df1', df2_name='df2'):\n    \"\"\"\n    So sánh dtype của các cột giữa hai DataFrame:\n    - Báo các cột chỉ có ở một trong hai DataFrame.\n    - Báo các cột có cùng tên nhưng dtype khác nhau.\n    \"\"\"\n\n    cols_df1 = set(df1.columns)\n    cols_df2 = set(df2.columns)\n\n    only_in_df1 = cols_df1 - cols_df2\n    only_in_df2 = cols_df2 - cols_df1\n    common_cols = cols_df1 & cols_df2\n\n    # 1. Báo cột chỉ có ở một bên\n    if only_in_df1:\n        print(f\"⚠️ Các cột chỉ có trong {df1_name} mà KHÔNG có trong {df2_name}: {only_in_df1}\")\n    if only_in_df2:\n        print(f\"⚠️ Các cột chỉ có trong {df2_name} mà KHÔNG có trong {df1_name}: {only_in_df2}\")\n    if not only_in_df1 and not only_in_df2:\n        print(\"✅ Hai DataFrame có cùng danh sách cột (không tính dtype).\")\n\n    # 2. So sánh dtype các cột trùng\n    dtype_mismatches = []\n    for col in common_cols:\n        dtype1 = df1[col].dtype\n        dtype2 = df2[col].dtype\n        if dtype1 != dtype2:\n            dtype_mismatches.append((col, dtype1, dtype2))\n\n    if dtype_mismatches:\n        print(f\"\\n🚨 Các cột có kiểu dữ liệu KHÔNG khớp giữa {df1_name} và {df2_name}:\")\n        for col, dtype1, dtype2 in dtype_mismatches:\n            print(f\"🔸 Cột '{col}': {df1_name} -> {dtype1}, {df2_name} -> {dtype2}\")\n    else:\n        print(\"\\n🎉 Tất cả các cột chung đều có dtype khớp nhau! Tuyệt zời ông mặt giời 🌞\")\n\n    return {\n        \"only_in_df1\": only_in_df1,\n        \"only_in_df2\": only_in_df2,\n        \"dtype_mismatches\": dtype_mismatches\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.529204Z","iopub.execute_input":"2025-06-18T08:48:22.529547Z","iopub.status.idle":"2025-06-18T08:48:22.537849Z","shell.execute_reply.started":"2025-06-18T08:48:22.529525Z","shell.execute_reply":"2025-06-18T08:48:22.536912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"compare_column_dtypes(df_7_9, df_10, df1_name='df_7_9', df2_name='df_10')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.538730Z","iopub.execute_input":"2025-06-18T08:48:22.539144Z","iopub.status.idle":"2025-06-18T08:48:22.568631Z","shell.execute_reply.started":"2025-06-18T08:48:22.539122Z","shell.execute_reply":"2025-06-18T08:48:22.567627Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# B. Tính Information value và loại bỏ các thuộc tính không đóng góp giá trị cho quá trình huấn luyện, chẳng hạn như các khóa ngoại","metadata":{}},{"cell_type":"markdown","source":"## 1. Kiểm tra ý nghĩa các thuộc tính để quyết định giữ lại hoặc loại bỏ","metadata":{}},{"cell_type":"code","source":"def plot_stacked_bar_top(df, target_col, label_col='label', top_n=30):\n    \"\"\"\n    Vẽ stacked bar chart phân phối label theo giá trị phổ biến nhất của 1 cột rời rạc.\n\n    Parameters:\n    - df: DataFrame đầu vào\n    - target_col: tên cột rời rạc muốn vẽ\n    - label_col: tên cột nhãn (default = 'label')\n    - top_n: số lượng giá trị phổ biến nhất muốn hiển thị (default = 30)\n    \"\"\"\n    # B1: Tính số lượng giá trị unique toàn cột\n    n_unique = df[target_col].nunique()\n\n    # B2: Lấy top N giá trị phổ biến nhất\n    top_values = df[target_col].value_counts().head(top_n).index\n\n    # B3: Lọc dữ liệu\n    df_top = df[df[target_col].isin(top_values)]\n\n    # B4: Nhóm và đếm theo target_col + label\n    grouped = df_top.groupby([target_col, label_col]).size().unstack(fill_value=0)\n\n    # Đảm bảo có đủ label\n    for lbl in [0, 1]:\n        if lbl not in grouped.columns:\n            grouped[lbl] = 0\n    grouped = grouped[[0, 1]]\n\n    # B5: Vẽ biểu đồ\n    plt.figure(figsize=(12, 6))\n    x = grouped.index.astype(str)\n\n    plt.bar(x, grouped[0], label='Label 0 (Không trễ)', color='mediumseagreen')\n    plt.bar(x, grouped[1], bottom=grouped[0], label='Label 1 (Bị trễ)', color='tomato')\n\n    # Thêm tổng số lên trên\n    for i in range(len(x)):\n        total = grouped.iloc[i].sum()\n        plt.text(i, total + 1, str(total), ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n    # Trang trí biểu đồ\n    plt.title(f'Phân phối Label theo {target_col} (Top {top_n} phổ biến)\\nSố lượng giá trị unique: {n_unique}', fontsize=14)\n    plt.xlabel(target_col)\n    plt.ylabel('Số lượng đơn hàng')\n    plt.xticks(rotation=45, ha='right')\n    plt.grid(axis='y', linestyle='--', alpha=0.6)\n    plt.legend()\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.570014Z","iopub.execute_input":"2025-06-18T08:48:22.570366Z","iopub.status.idle":"2025-06-18T08:48:22.591496Z","shell.execute_reply.started":"2025-06-18T08:48:22.570332Z","shell.execute_reply":"2025-06-18T08:48:22.590307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Lọc cột hợp lệ \n# valid_cols = [col for col in df_7_9.columns]\n\n# # Vẽ biểu đồ cho mỗi cột hợp lệ\n# for col in valid_cols:\n\n#     print(f'\\n📦 Đang vẽ cho cột: {col} - DataFrame: df_7_9')\n#     plot_stacked_bar_top(df_7_9, col)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.592599Z","iopub.execute_input":"2025-06-18T08:48:22.592909Z","iopub.status.idle":"2025-06-18T08:48:22.615008Z","shell.execute_reply.started":"2025-06-18T08:48:22.592854Z","shell.execute_reply":"2025-06-18T08:48:22.613781Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Tính Information value của các cột","metadata":{}},{"cell_type":"markdown","source":"| Giá trị IV     | Ý nghĩa                                                       |\n| -------------- | ------------------------------------------------------------- |\n| **< 0.02**     | Không liên quan đến mục tiêu (noise, bỏ được)            |\n| **0.02 - 0.1** | Liên quan yếu (nếu thiếu biến thì giữ, còn không thì thôi)  |\n| **0.1 - 0.3**  | Liên quan trung bình – dùng được, đáng cân nhắc!           |\n| **0.3 - 0.5**  | Rất tốt, biến có sức phân biệt mạnh mẽ                      |\n| **> 0.5**      | Cực mạnh – nhưng cẩn thận vì có thể **quá khớp (overfit)**  |\n\n💣 Lưu ý: các trường hợp làm IV cao bất thường:\n\nHigh cardinality: quá nhiều giá trị rời rạc như CODE, khóa ngoại, mỗi giá trị ít mẫu.\n\n- Overbinning: chia quá nhiều bin, mỗi bin nhỏ xíu.\n\n- Trùng khớp ngẫu nhiên: giá trị tình cờ trùng với target.\n\n- Data leakage: biến chứa thông tin của biến mục tiêu.\n\n- Mẫu nhỏ / mất cân bằng: dữ liệu không đủ ổn định để tính WOE.","metadata":{}},{"cell_type":"code","source":"def iv_woe(data, target, bins=10, show_woe=False):\n    \n    #Empty Dataframe\n    newDF,woeDF = pd.DataFrame(), pd.DataFrame()\n    \n    #Extract Column Names\n    cols = data.columns\n    \n    #Run WOE and IV on all the independent variables\n    for ivars in cols[~cols.isin([target])]:\n        if (data[ivars].dtype.kind in 'bifc') and (len(np.unique(data[ivars]))>10):\n            binned_x = pd.qcut(data[ivars], bins,  duplicates='drop')\n            d0 = pd.DataFrame({'x': binned_x, 'y': data[target]})\n        else:\n            d0 = pd.DataFrame({'x': data[ivars], 'y': data[target]})\n        d0 = d0.astype({\"x\": str})\n        d = d0.groupby(\"x\", as_index=False, dropna=False).agg({\"y\": [\"count\", \"sum\"]})\n        d.columns = ['Cutoff', 'N', 'Events']\n        d['% of Events'] = np.maximum(d['Events'], 0.5) / d['Events'].sum()\n        d['Non-Events'] = d['N'] - d['Events']\n        d['% of Non-Events'] = np.maximum(d['Non-Events'], 0.5) / d['Non-Events'].sum()\n        d['WoE'] = np.log(d['% of Non-Events']/d['% of Events'])\n        d['IV'] = d['WoE'] * (d['% of Non-Events']-d['% of Events'])\n        d.insert(loc=0, column='Variable', value=ivars)\n        # print(\"Information value of \" + ivars + \" is \" + str(round(d['IV'].sum(),6)))\n        temp =pd.DataFrame({\"Variable\" : [ivars], \"IV\" : [d['IV'].sum()]}, columns = [\"Variable\", \"IV\"])\n        newDF=pd.concat([newDF,temp], axis=0)\n        woeDF=pd.concat([woeDF,d], axis=0)\n\n        #Show WOE Table\n        if show_woe == True:\n            print(d)\n    return newDF, woeDF","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.616195Z","iopub.execute_input":"2025-06-18T08:48:22.616668Z","iopub.status.idle":"2025-06-18T08:48:22.639608Z","shell.execute_reply.started":"2025-06-18T08:48:22.616637Z","shell.execute_reply":"2025-06-18T08:48:22.638649Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Tính IV trên bộ dữ liệu 7-9","metadata":{}},{"cell_type":"code","source":"iv_full, woe_full = iv_woe(data=df_7_9, target='label', bins=10, show_woe=False)\niv_sorted = iv_full.sort_values(by='IV', ascending=False).reset_index(drop=True)\n\nprint(iv_sorted)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:22.640561Z","iopub.execute_input":"2025-06-18T08:48:22.640895Z","iopub.status.idle":"2025-06-18T08:48:41.824122Z","shell.execute_reply.started":"2025-06-18T08:48:22.640848Z","shell.execute_reply":"2025-06-18T08:48:41.822963Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Loại bỏ các trường dữ liệu không đóng góp giá trị cho quá trình huấn luyện","metadata":{}},{"cell_type":"markdown","source":"**Danh sách các trường có IV thấp hơn <0.05**","metadata":{}},{"cell_type":"code","source":"# Ngưỡng IV thấp\niv_threshold = 0.05\n\n# Lấy danh sách các biến có IV thấp hơn ngưỡng\nlow_iv_vars = iv_sorted[iv_sorted['IV'] < iv_threshold]['Variable'].tolist()\n\n# In ra (nếu muốn kiểm tra)\nprint(\"Các trường có IV thấp hơn 0.05:\", low_iv_vars) ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:41.825124Z","iopub.execute_input":"2025-06-18T08:48:41.825477Z","iopub.status.idle":"2025-06-18T08:48:41.833112Z","shell.execute_reply.started":"2025-06-18T08:48:41.825450Z","shell.execute_reply":"2025-06-18T08:48:41.832044Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Danh sách các cột cần loại bỏ\ncols_to_remove = ['GLOBAL_NO', # Vì trong df_7_9 đây là khóa chính\n                  'SHIP DECISION NO', # vì dữ liệu chỉ tập trung vào 1 giá trị (-1) ở cả 2 bộ data và giá trị unique cực cao\n                  #'PRODUCT_CD', # Giá trị unique cực kì cao\n                  #'INNER_CD', # Giá trị unique cực kì cao\n                  'SO_TIME', # kiểu time \n                  'order_dayofweek', # trùng với SO_DAY_OF_WEEK \n                  'so_second', # đơn vị quá nhỏ không có ý nghĩa \n                  'so_minute ', # đơn vị quá nhỏ không có ý nghĩa \n                  'Order date', # Kiểu datetime\n                  'VSD', # Kiểu datetime\n                  'SUPPLIER_DIV',\n                  # 'CUST_CD', # giá trị unique cực kì cao\n                  ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:41.834073Z","iopub.execute_input":"2025-06-18T08:48:41.834431Z","iopub.status.idle":"2025-06-18T08:48:41.856804Z","shell.execute_reply.started":"2025-06-18T08:48:41.834409Z","shell.execute_reply":"2025-06-18T08:48:41.855833Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Gộp 2 danh sách và loại bỏ trùng lặp (đặc biệt lưu ý xóa khoảng trắng thừa!)\ncombined_cols_to_drop = set([col.strip() for col in cols_to_remove + low_iv_vars])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:41.857817Z","iopub.execute_input":"2025-06-18T08:48:41.858146Z","iopub.status.idle":"2025-06-18T08:48:41.875692Z","shell.execute_reply.started":"2025-06-18T08:48:41.858120Z","shell.execute_reply":"2025-06-18T08:48:41.874928Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def drop_useless_columns(df, columns_to_drop, inplace=False, verbose=True):\n    \"\"\"\n    Loại bỏ các cột không đóng góp giá trị cho quá trình huấn luyện, ví dụ: ID, khóa ngoại, mã code...\n\n    Parameters:\n    - df: DataFrame cần xử lý\n    - columns_to_drop: Danh sách tên cột cần loại bỏ (list hoặc set)\n    - inplace: Nếu True thì thay đổi trực tiếp trên df, nếu False thì trả về bản sao\n    - verbose: Nếu True thì in ra các cột đã bị loại bỏ\n\n    Returns:\n    - df_clean: DataFrame sau khi loại bỏ (nếu inplace=False)\n    \"\"\"\n    existing_cols = [col for col in columns_to_drop if col in df.columns]\n    \n    if verbose:\n        if existing_cols:\n            print(f\"🧹 Đang loại bỏ các cột: {existing_cols}\")\n        else:\n            print(\"✅ Không có cột nào trong danh sách cần loại bỏ tồn tại trong DataFrame!\")\n\n    if inplace:\n        df.drop(columns=existing_cols, inplace=True)\n    else:\n        return df.drop(columns=existing_cols)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:41.876924Z","iopub.execute_input":"2025-06-18T08:48:41.877220Z","iopub.status.idle":"2025-06-18T08:48:41.895256Z","shell.execute_reply.started":"2025-06-18T08:48:41.877194Z","shell.execute_reply":"2025-06-18T08:48:41.894315Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Áp dụng hàm xóa\n# drop_useless_columns(df_4_6, combined_cols_to_drop, inplace=True)\ndrop_useless_columns(df_7_9, combined_cols_to_drop, inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:41.895903Z","iopub.execute_input":"2025-06-18T08:48:41.896260Z","iopub.status.idle":"2025-06-18T08:48:42.027478Z","shell.execute_reply.started":"2025-06-18T08:48:41.896230Z","shell.execute_reply":"2025-06-18T08:48:42.026355Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"drop_useless_columns(df_10, combined_cols_to_drop, inplace=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.028522Z","iopub.execute_input":"2025-06-18T08:48:42.028989Z","iopub.status.idle":"2025-06-18T08:48:42.051746Z","shell.execute_reply.started":"2025-06-18T08:48:42.028965Z","shell.execute_reply":"2025-06-18T08:48:42.050783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_7_9.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.052815Z","iopub.execute_input":"2025-06-18T08:48:42.053106Z","iopub.status.idle":"2025-06-18T08:48:42.410249Z","shell.execute_reply.started":"2025-06-18T08:48:42.053085Z","shell.execute_reply":"2025-06-18T08:48:42.408996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_10.info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.411212Z","iopub.execute_input":"2025-06-18T08:48:42.411476Z","iopub.status.idle":"2025-06-18T08:48:42.478718Z","shell.execute_reply.started":"2025-06-18T08:48:42.411453Z","shell.execute_reply":"2025-06-18T08:48:42.477945Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# C. Chia Train Dev Test và Cân bằng lại dữ liệu","metadata":{}},{"cell_type":"markdown","source":"## 1. Chia Train Dev Test và Cân bằng dữ liệu","metadata":{}},{"cell_type":"markdown","source":"### Hàm chia Train Dev Test","metadata":{}},{"cell_type":"code","source":"def split_data(df, label_col='label', test_size=0.2, val_size=0.1, random_state=42):\n    \"\"\"\n    Chia một DataFrame thành ba tập: train, validation và test, giữ phân phối lớp không đổi (stratify).\n\n    Parameters:\n        df (pd.DataFrame): Dữ liệu đầu vào có cột nhãn.\n        label_col (str): Tên cột nhãn (default='label').\n        test_size (float): Tỷ lệ tập test trên toàn bộ dữ liệu (default=0.2).\n        val_size (float): Tỷ lệ tập validation trên toàn bộ dữ liệu (default=0.1).\n        random_state (int): Seed để tái tạo kết quả chia (default=42).\n\n    Returns:\n        df_train (pd.DataFrame): Tập huấn luyện.\n        df_val (pd.DataFrame): Tập validation.\n        df_test (pd.DataFrame): Tập kiểm tra.\n    \"\"\"\n    X = df.drop(columns=[label_col])\n    y = df[label_col]\n\n    # Chia train+val và test\n    X_temp, X_test, y_temp, y_test = train_test_split(X, y, \n                                                      test_size=test_size, \n                                                      stratify=y,\n                                                      random_state=random_state)\n    \n    # Chia train và val\n    val_ratio = val_size / (1 - test_size)\n    X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, \n                                                      test_size=val_ratio, \n                                                      stratify=y_temp,\n                                                      random_state=random_state)\n    \n    # Kết hợp lại thành df\n    df_train = X_train.copy()\n    df_train[label_col] = y_train\n\n    df_val = X_val.copy()\n    df_val[label_col] = y_val\n\n    df_test = X_test.copy()\n    df_test[label_col] = y_test\n\n    return df_train, df_val, df_test","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.479673Z","iopub.execute_input":"2025-06-18T08:48:42.479973Z","iopub.status.idle":"2025-06-18T08:48:42.488471Z","shell.execute_reply.started":"2025-06-18T08:48:42.479951Z","shell.execute_reply":"2025-06-18T08:48:42.487474Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Hàm thực hiện under sampling","metadata":{}},{"cell_type":"markdown","source":">Trong giai đoạn này, chúng tôi áp dụng kỹ thuật undersampling với nhiều tỉ lệ khác nhau để xử lý mất cân bằng dữ liệu. Over sampling sẽ được sử dụng trong phần cải tiến mô hình.","metadata":{}},{"cell_type":"code","source":"def undersample_df(df, label_col='label', ratio=1/10, random_state=42):\n    \"\"\"\n    Thực hiện under sampling lớp đa số để đạt tỷ lệ mong muốn so với lớp thiểu số.\n\n    Parameters:\n        df (pd.DataFrame): Dữ liệu đầu vào có cột nhãn.\n        label_col (str): Tên cột nhãn (default='label').\n        ratio (float): Tỷ lệ giữa lớp thiểu số / lớp đa số mong muốn (default=1/10).\n        random_state (int): Seed để tái tạo kết quả lấy mẫu (default=42).\n\n    Returns:\n        df_balanced (pd.DataFrame): DataFrame sau khi under sampling, đã được shuffle.\n    \"\"\"\n    label_counts = df[label_col].value_counts()\n    minority_label = label_counts.idxmin()\n    majority_label = label_counts.idxmax()\n    \n    n_minority = label_counts.min()\n    n_majority = int(n_minority / ratio)\n\n    df_minority = df[df[label_col] == minority_label]\n    df_majority = df[df[label_col] == majority_label]\n\n    df_majority_downsampled = resample(df_majority,\n                                       replace=False,\n                                       n_samples=n_majority,\n                                       random_state=random_state)\n\n    df_balanced = pd.concat([df_minority, df_majority_downsampled])\n    return df_balanced.sample(frac=1, random_state=random_state).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.489408Z","iopub.execute_input":"2025-06-18T08:48:42.489713Z","iopub.status.idle":"2025-06-18T08:48:42.512880Z","shell.execute_reply.started":"2025-06-18T08:48:42.489688Z","shell.execute_reply":"2025-06-18T08:48:42.511905Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Hàm thực hiện under sampling trên các tập train","metadata":{}},{"cell_type":"code","source":"def split_and_undersample(df, label_col='label', test_size=0.2, val_size=0.1, ratios=[1/10, 1/20, 1/5]):\n    \"\"\"\n    Chia tập dữ liệu thành train/val/test và thực hiện under sampling trên tập train với nhiều tỷ lệ.\n\n    Parameters:\n        df (pd.DataFrame): Dữ liệu đầu vào có cột nhãn.\n        label_col (str): Tên cột nhãn (default='label').\n        test_size (float): Tỷ lệ tập test trên toàn bộ dữ liệu (default=0.2).\n        val_size (float): Tỷ lệ tập validation trên toàn bộ dữ liệu (default=0.1).\n        ratios (list of float): Danh sách các tỷ lệ thiểu số/đa số cần under sampling (default=[1/10, 1/20, 1/5]).\n\n    Returns:\n        dict: Từ điển với các key:\n            - 'train_under_10', 'train_under_20', ...: tập train sau khi under sampling.\n            - 'val': tập validation.\n            - 'test': tập kiểm tra.\n    \"\"\"\n    df_train, df_val, df_test = split_data(df, label_col=label_col, test_size=test_size, val_size=val_size)\n\n    result = {}\n    for r in ratios:\n        undersampled_train = undersample_df(df_train, label_col=label_col, ratio=r)\n        key = f'train_under_{int(1/r)}'\n        result[key] = undersampled_train\n\n    result['val'] = df_val\n    result['test'] = df_test\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.513682Z","iopub.execute_input":"2025-06-18T08:48:42.514042Z","iopub.status.idle":"2025-06-18T08:48:42.538085Z","shell.execute_reply.started":"2025-06-18T08:48:42.514016Z","shell.execute_reply":"2025-06-18T08:48:42.537115Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Chia và under df_7_9\nresults_7_9 = split_and_undersample(df_7_9, ratios=[1/10, 1/20, 1/5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:42.538975Z","iopub.execute_input":"2025-06-18T08:48:42.539255Z","iopub.status.idle":"2025-06-18T08:48:44.975301Z","shell.execute_reply.started":"2025-06-18T08:48:42.539233Z","shell.execute_reply":"2025-06-18T08:48:44.974415Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# df_7_9\ntrain_7_9_1_5  = results_7_9['train_under_5']\ntrain_7_9_1_10 = results_7_9['train_under_10']\ntrain_7_9_1_20 = results_7_9['train_under_20']\nval_7_9 = results_7_9['val']\ntest_7_9 = results_7_9['test']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:44.976119Z","iopub.execute_input":"2025-06-18T08:48:44.976450Z","iopub.status.idle":"2025-06-18T08:48:44.981167Z","shell.execute_reply.started":"2025-06-18T08:48:44.976387Z","shell.execute_reply":"2025-06-18T08:48:44.980265Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(val_7_9.isnull().sum().sort_values(ascending=False).head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:44.982201Z","iopub.execute_input":"2025-06-18T08:48:44.982524Z","iopub.status.idle":"2025-06-18T08:48:45.053234Z","shell.execute_reply.started":"2025-06-18T08:48:44.982497Z","shell.execute_reply":"2025-06-18T08:48:45.052133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# D. Huấn luyện mô hình và encoding các thuộc tính rời rạc nếu cần thiết","metadata":{}},{"cell_type":"code","source":"# def cast_category_dtype(*dfs):\n#     \"\"\"\n#     Trả về các DataFrame mới đã ép các cột object -> category.\n#     \"\"\"\n#     converted = []\n#     for df in dfs:\n#         df_copy = df.copy()\n#         for col in df_copy.select_dtypes(['object']).columns:\n#             df_copy[col] = df_copy[col].astype('category')\n#         converted.append(df_copy)\n#     return converted","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:45.054329Z","iopub.execute_input":"2025-06-18T08:48:45.054673Z","iopub.status.idle":"2025-06-18T08:48:45.063329Z","shell.execute_reply.started":"2025-06-18T08:48:45.054643Z","shell.execute_reply":"2025-06-18T08:48:45.062350Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## II/ LIGHTGBM","metadata":{}},{"cell_type":"code","source":"# hàm chuyển đổi kiểu dữ liệu object thành categorical - phục vụ cho một số mô hình cần phân biệt categorical\ndef align_category_columns(train_df, val_df, test_df): \n    # Lấy các cột object hoặc category từ train\n    cat_cols = train_df.select_dtypes(include=['object', 'category']).columns.tolist()\n\n    for col in cat_cols:\n        # Ép kiểu category cho tất cả\n        categories = train_df[col].astype('category').cat.categories\n        train_df[col] = train_df[col].astype('category').cat.set_categories(categories)\n        val_df[col] = val_df[col].astype('category').cat.set_categories(categories)\n        test_df[col] = test_df[col].astype('category').cat.set_categories(categories)\n    \n    return train_df, val_df, test_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:45.064481Z","iopub.execute_input":"2025-06-18T08:48:45.064827Z","iopub.status.idle":"2025-06-18T08:48:45.092417Z","shell.execute_reply.started":"2025-06-18T08:48:45.064798Z","shell.execute_reply":"2025-06-18T08:48:45.091048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_lightgbm(X_train, y_train, X_val, y_val, X_test, y_test):\n    \"\"\"\n    Huấn luyện mô hình LGBMClassifier, tự động xử lý categorical features, \n    và in báo cáo phân loại & AUC trên tập test.\n    \"\"\"\n\n    # 🔍 Tìm các cột dạng object/category\n    categorical_cols = X_train.select_dtypes(include=['object', 'category']).columns\n    print(f\"📌 Categorical columns detected: {list(categorical_cols)}\")\n\n    # 🔄 Chuyển sang category dtype\n    for col in categorical_cols:\n        X_train[col] = X_train[col].astype('category')\n        X_val[col]   = X_val[col].astype('category')\n        X_test[col]  = X_test[col].astype('category')\n\n    # 🌟 Khởi tạo mô hình\n    model = LGBMClassifier(\n        n_estimators=1000,\n        learning_rate=0.05,\n        max_depth=6,\n        objective='binary',\n        random_state=42\n    )\n\n    # 🚀 Huấn luyện\n    model.fit(\n    X_train, y_train,\n    eval_set=[(X_val, y_val)],\n    eval_metric='auc',\n    categorical_feature=categorical_cols.tolist(),\n    callbacks=[\n        early_stopping(stopping_rounds=50),\n        log_evaluation(period=100)\n        ]\n    )\n\n\n    # 🔮 Dự đoán\n    y_pred = model.predict(X_test)\n    y_proba = model.predict_proba(X_test)[:, 1]\n\n    # 📊 Báo cáo\n    print(\"\\n📊 Classification Report (Test Set):\")\n    print(classification_report(y_test, y_pred))\n\n    # 🎯 AUC\n    auc = roc_auc_score(y_test, y_proba)\n    print(f\"🎯 ROC AUC: {auc:.4f}\")\n\n    return model\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:45.093344Z","iopub.execute_input":"2025-06-18T08:48:45.093611Z","iopub.status.idle":"2025-06-18T08:48:45.107768Z","shell.execute_reply.started":"2025-06-18T08:48:45.093589Z","shell.execute_reply":"2025-06-18T08:48:45.106778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Hàm tách các biến độc lập và phụ thuộc để chạy LightGBM\ndef run_lightgbm_from_df(train_df, val_df, test_df, label_col='label'):\n    \"\"\"\n    Parameters:\n        train_df (pd.DataFrame): Tập dữ liệu huấn luyện, bao gồm cột nhãn.\n        val_df (pd.DataFrame): Tập dữ liệu validation, bao gồm cột nhãn.\n        test_df (pd.DataFrame): Tập dữ liệu kiểm tra, bao gồm cột nhãn.\n        label_col (str): Tên cột chứa nhãn phân loại (default='label').\n\n    Returns:\n        model: Mô hình LightGBM đã được huấn luyện.\n    \"\"\"\n    return train_lightgbm(\n        train_df.drop(columns=label_col), train_df[label_col],\n        val_df.drop(columns=label_col), val_df[label_col],\n        test_df.drop(columns=label_col), test_df[label_col]\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:45.108845Z","iopub.execute_input":"2025-06-18T08:48:45.109172Z","iopub.status.idle":"2025-06-18T08:48:45.131789Z","shell.execute_reply.started":"2025-06-18T08:48:45.109150Z","shell.execute_reply":"2025-06-18T08:48:45.130685Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train Dev Test Trên tập 7-9","metadata":{}},{"cell_type":"code","source":"print(\"\\n==================== 🧪 TỈ LỆ 1:5 ====================\")\ntrain_7_9_1_5, val_7_9, test_7_9 = align_category_columns(train_7_9_1_5, val_7_9, test_7_9)\nmodel_lightgbm_7_9_1_5 = run_lightgbm_from_df(train_7_9_1_5, val_7_9, test_7_9)\n\nprint(\"\\n==================== 🧪 TỈ LỆ 1:10 ====================\")\ntrain_7_9_1_10, val_7_9, test_7_9 = align_category_columns(train_7_9_1_10, val_7_9, test_7_9)\nmodel_lightgbm_7_9_1_10 = run_lightgbm_from_df(train_7_9_1_10, val_7_9, test_7_9)\n\nprint(\"\\n==================== 🧪 TỈ LỆ 1:20 ====================\")\ntrain_7_9_1_20, val_7_9, test_7_9 = align_category_columns(train_7_9_1_20, val_7_9, test_7_9)\nmodel_lightgbm_7_9_1_20 = run_lightgbm_from_df(train_7_9_1_20, val_7_9, test_7_9)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:48:45.132837Z","iopub.execute_input":"2025-06-18T08:48:45.133160Z","iopub.status.idle":"2025-06-18T08:50:21.293684Z","shell.execute_reply.started":"2025-06-18T08:48:45.133129Z","shell.execute_reply":"2025-06-18T08:50:21.292297Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dự Đoán Cho Dữ Liệu Tháng 10","metadata":{}},{"cell_type":"code","source":"model_to_use = model_lightgbm_7_9_1_10\ncorresponding_training_set = train_7_9_1_10\n\n# Lấy danh sách các cột features mà mô hình đã được huấn luyện\nfeatures_columns = corresponding_training_set.drop(columns=['label']).columns\n\nprint(f\"Đang sử dụng model được train trên tập: train_7_9_1_10\")\nprint(f\"Các features mà mô hình sử dụng để dự đoán: {len(features_columns)} cột\")\n\n# Chuẩn bị dữ liệu df_10 cho việc dự đoán\n# Thêm .copy() để tránh các cảnh báo khi thay đổi dữ liệu ở bước sau\nids_10 = df_10['ID']\ndf_10_prepared = df_10[features_columns].copy()\n\nprint(f\"\\nĐã chuẩn bị xong dữ liệu df_10 với {df_10_prepared.shape[1]} features.\")\n\n# ĐỒNG BỘ KIỂU DỮ LIỆU CATEGORICAL\nprint(\"\\nBắt đầu đồng bộ kiểu dữ liệu categorical...\")\n# Lấy các cột categorical từ tập train gốc để làm tham chiếu\ncat_cols = corresponding_training_set.select_dtypes(include=['object', 'category']).columns\n\nfor col in cat_cols:\n    # Lấy các \"loại\" (categories) đã biết từ tập train\n    known_categories = corresponding_training_set[col].astype('category').cat.categories\n    \n    # Ép kiểu và áp dụng các \"loại\" đã biết đó vào cột tương ứng của df_10\n    # Điều này đảm bảo df_10 có chính xác các category mà mô hình đã học\n    df_10_prepared[col] = df_10_prepared[col].astype('category').cat.set_categories(known_categories)\n\nprint(f\"Đồng bộ hoàn tất cho {len(cat_cols)} cột: {list(cat_cols)}\")\n# KẾT THÚC BƯỚC MỚI \n\n# == DỰ ĐOÁN VÀ XUẤT KẾT QUẢ ===\n\n# Dùng mô hình đã chọn để dự đoán trên dữ liệu đã chuẩn bị\nprint(\"\\nBắt đầu dự đoán trên bộ dữ liệu tháng 10...\")\npredicted_labels = model_to_use.predict(df_10_prepared)\nprint(\"Dự đoán hoàn tất!\")\n\n# Tạo DataFrame kết quả\noutput_df_10 = pd.DataFrame({\n    'ID': ids_10,\n    'label': predicted_labels\n})\n\n# Lưu DataFrame kết quả ra file CSV\noutput_filename = '23521041_LuongDacNguyen_6422.csv'\noutput_df_10.to_csv(output_filename, index=False)\n\nprint(f\"\\nĐã lưu kết quả thành công vào file: {output_filename}\")\nprint(\"Xem trước 5 dòng đầu của file kết quả:\")\nprint(output_df_10.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T08:50:21.294739Z","iopub.execute_input":"2025-06-18T08:50:21.295323Z","iopub.status.idle":"2025-06-18T08:50:27.496639Z","shell.execute_reply.started":"2025-06-18T08:50:21.295289Z","shell.execute_reply":"2025-06-18T08:50:27.495716Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}