{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":10338,"databundleVersionId":862042},{"sourceType":"competition","sourceId":24800,"datasetId":1042002,"databundleVersionId":1831594},{"sourceType":"datasetVersion","sourceId":1317101,"datasetId":642388,"databundleVersionId":1349348}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ============================================================\n# CELL 1: IMPORT THƯ VIỆN VÀ CẤU HÌNH CHUNG\n# ============================================================\n\nimport os\nimport random\nimport shutil\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.model_selection import train_test_split\nfrom PIL import Image\n\n# Seed cố định để việc chia train/val/test có thể tái lập\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\n\n# Thư mục output để lưu metadata CSV\nOUTPUT_DIR = Path(\"/kaggle/working/metadata\")\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\nprint(\"OUTPUT_DIR:\", OUTPUT_DIR)\nprint(\"Notebook 01 setup completed.\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-17T00:41:08.013683Z","iopub.execute_input":"2026-05-17T00:41:08.014116Z","iopub.status.idle":"2026-05-17T00:41:09.053661Z","shell.execute_reply.started":"2026-05-17T00:41:08.014056Z","shell.execute_reply":"2026-05-17T00:41:09.052554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 2 - FIXED: KIỂM TRA CẤU TRÚC /kaggle/input\n# ============================================================\n\nfrom pathlib import Path\n\nINPUT_ROOT = Path(\"/kaggle/input\")\n\nprint(\"Danh sách cấp 1 trong /kaggle/input:\")\nfor p in sorted(INPUT_ROOT.iterdir()):\n    print(\"-\", p.name)\n\nprint(\"\\nDanh sách sâu hơn trong /kaggle/input:\")\nfor p in sorted(INPUT_ROOT.rglob(\"*\")):\n    if p.is_dir():\n        rel = p.relative_to(INPUT_ROOT)\n        depth = len(rel.parts)\n        \n        # Chỉ in đến độ sâu 3 để tránh quá dài\n        if depth <= 3:\n            print(\"-\", rel)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-17T00:41:29.930014Z","iopub.execute_input":"2026-05-17T00:41:29.930487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 3 - FIXED: TỰ ĐỘNG TÌM ĐƯỜNG DẪN DATASET\n# Hỗ trợ cả cấu trúc:\n# /kaggle/input/ten-dataset\n# /kaggle/input/competitions/ten-competition\n# /kaggle/input/datasets/owner/ten-dataset\n# ============================================================\n\ndef find_input_dir_deep(keywords, root=INPUT_ROOT, max_depth=4):\n    \"\"\"\n    Tìm thư mục dataset trong /kaggle/input theo keyword.\n    Hàm này tìm sâu hơn để hỗ trợ Kaggle mount dạng competitions/datasets.\n    \n    keywords: list từ khóa, ví dụ [\"rsna\", \"pneumonia\"]\n    max_depth: giới hạn độ sâu để tránh quét quá rộng\n    \"\"\"\n    keywords = [k.lower() for k in keywords]\n    candidates = []\n    \n    for p in root.rglob(\"*\"):\n        if not p.is_dir():\n            continue\n        \n        rel = p.relative_to(root)\n        depth = len(rel.parts)\n        \n        if depth > max_depth:\n            continue\n        \n        name = str(rel).lower()\n        \n        if all(k in name for k in keywords):\n            candidates.append(p)\n    \n    return candidates\n\n\n# Tìm ứng viên cho từng dataset\nrsna_candidates = find_input_dir_deep([\"rsna\", \"pneumonia\"])\nnih_candidates = find_input_dir_deep([\"nih\", \"chest\"])\nvindr_candidates = find_input_dir_deep([\"vinbigdata\"])\n\nprint(\"RSNA candidates:\")\nfor c in rsna_candidates:\n    print(\"-\", c)\n\nprint(\"\\nNIH candidates:\")\nfor c in nih_candidates:\n    print(\"-\", c)\n\nprint(\"\\nVinDr/VinBigData candidates:\")\nfor c in vindr_candidates:\n    print(\"-\", c)\n\n\n# Chọn candidate phù hợp nhất\n# Với competition/dataset, thư mục cha chứa file train.csv hoặc stage_2_train_labels.csv thường là thư mục đúng.\nRSNA_DIR = rsna_candidates[0] if len(rsna_candidates) > 0 else None\nNIH_DIR = nih_candidates[0] if len(nih_candidates) > 0 else None\nVINDR_DIR = vindr_candidates[0] if len(vindr_candidates) > 0 else None\n\nprint(\"\\nSelected paths:\")\nprint(\"RSNA_DIR :\", RSNA_DIR)\nprint(\"NIH_DIR  :\", NIH_DIR)\nprint(\"VINDR_DIR:\", VINDR_DIR)\n\nassert RSNA_DIR is not None, \"Không tìm thấy RSNA dataset. Hãy kiểm tra Add Input.\"\nassert NIH_DIR is not None, \"Không tìm thấy NIH dataset. Hãy kiểm tra Add Input.\"\nassert VINDR_DIR is not None, \"Không tìm thấy VinBigData dataset. Hãy kiểm tra Add Input.\"\n\nprint(\"\\nĐã tìm thấy đủ 3 dataset.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 4: XEM CẤU TRÚC FILE CỦA TỪNG DATASET\n# ============================================================\n\ndef show_tree(root, max_depth=2, max_items=80):\n    \"\"\"\n    In cây thư mục rút gọn để xem cấu trúc dataset.\n    max_depth: độ sâu tối đa.\n    max_items: số item tối đa in ra.\n    \"\"\"\n    root = Path(root)\n    count = 0\n    \n    print(f\"\\n===== {root} =====\")\n    \n    for path in sorted(root.rglob(\"*\")):\n        rel = path.relative_to(root)\n        depth = len(rel.parts)\n        \n        if depth > max_depth:\n            continue\n        \n        indent = \"  \" * (depth - 1)\n        suffix = \"/\" if path.is_dir() else \"\"\n        print(f\"{indent}- {path.name}{suffix}\")\n        \n        count += 1\n        if count >= max_items:\n            print(\"... output truncated ...\")\n            break\n\nshow_tree(RSNA_DIR, max_depth=2, max_items=80)\nshow_tree(NIH_DIR, max_depth=2, max_items=80)\nshow_tree(VINDR_DIR, max_depth=2, max_items=80)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 5: TÌM FILE NHÃN VÀ THƯ MỤC ẢNH RSNA\n# ============================================================\n\ndef find_file(root, filename):\n    matches = list(Path(root).rglob(filename))\n    return matches[0] if matches else None\n\ndef find_dir(root, dirname):\n    matches = [p for p in Path(root).rglob(dirname) if p.is_dir()]\n    return matches[0] if matches else None\n\nRSNA_LABEL_CSV = find_file(RSNA_DIR, \"stage_2_train_labels.csv\")\nRSNA_CLASS_INFO_CSV = find_file(RSNA_DIR, \"stage_2_detailed_class_info.csv\")\nRSNA_TRAIN_IMG_DIR = find_dir(RSNA_DIR, \"stage_2_train_images\")\n\nprint(\"RSNA_LABEL_CSV     :\", RSNA_LABEL_CSV)\nprint(\"RSNA_CLASS_INFO_CSV:\", RSNA_CLASS_INFO_CSV)\nprint(\"RSNA_TRAIN_IMG_DIR :\", RSNA_TRAIN_IMG_DIR)\n\nassert RSNA_LABEL_CSV is not None, \"Không tìm thấy stage_2_train_labels.csv\"\nassert RSNA_TRAIN_IMG_DIR is not None, \"Không tìm thấy stage_2_train_images\"\n\nprint(\"RSNA paths OK.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 6: ĐỌC NHÃN RSNA\n# ============================================================\n\nrsna_labels = pd.read_csv(RSNA_LABEL_CSV)\n\nprint(\"Shape rsna_labels:\", rsna_labels.shape)\ndisplay(rsna_labels.head())\n\nprint(\"\\nCác cột trong rsna_labels:\")\nprint(rsna_labels.columns.tolist())\n\nprint(\"\\nPhân bố Target theo dòng bbox:\")\nprint(rsna_labels[\"Target\"].value_counts(dropna=False))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 7: GOM NHÃN RSNA THEO PATIENTID\n# ============================================================\n\n# Gom label: max(Target) theo patientId\nrsna_grouped = (\n    rsna_labels\n    .groupby(\"patientId\")\n    .agg(\n        label=(\"Target\", \"max\"),\n        num_boxes=(\"Target\", \"sum\")\n    )\n    .reset_index()\n)\n\n# Tạo đường dẫn ảnh DICOM\nrsna_grouped[\"image_path\"] = rsna_grouped[\"patientId\"].apply(\n    lambda x: str(RSNA_TRAIN_IMG_DIR / f\"{x}.dcm\")\n)\n\n# Kiểm tra file có tồn tại không\nrsna_grouped[\"exists\"] = rsna_grouped[\"image_path\"].apply(lambda x: Path(x).exists())\n\n# Chỉ giữ ảnh tồn tại\nrsna_meta = rsna_grouped[rsna_grouped[\"exists\"]].copy()\nrsna_meta = rsna_meta.drop(columns=[\"exists\"])\n\nprint(\"Số ảnh RSNA sau khi gom patientId:\", len(rsna_meta))\nprint(\"\\nPhân bố label RSNA:\")\nprint(rsna_meta[\"label\"].value_counts())\n\ndisplay(rsna_meta.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 8: LƯU BOUNDING BOX RSNA CHO PHÂN TÍCH GRAD-CAM SAU NÀY\n# ============================================================\n\nbbox_cols = [\"patientId\", \"x\", \"y\", \"width\", \"height\", \"Target\"]\n\nrsna_bboxes = rsna_labels[bbox_cols].copy()\n\n# Chỉ bbox thật sự có Target = 1 mới có ý nghĩa vùng viêm phổi\nrsna_bboxes_pos = rsna_bboxes[rsna_bboxes[\"Target\"] == 1].copy()\n\nbbox_output_path = OUTPUT_DIR / \"rsna_bboxes.csv\"\nrsna_bboxes_pos.to_csv(bbox_output_path, index=False)\n\nprint(\"Saved:\", bbox_output_path)\nprint(\"Positive bbox rows:\", len(rsna_bboxes_pos))\ndisplay(rsna_bboxes_pos.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 9: CHIA RSNA TRAIN / VAL / TEST\n# ============================================================\n\n# Chia train_temp và test trước: test = 15%\nrsna_train_val, rsna_test = train_test_split(\n    rsna_meta,\n    test_size=0.15,\n    random_state=SEED,\n    stratify=rsna_meta[\"label\"]\n)\n\n# Từ phần còn lại, chia val = 15% tổng.\n# Vì train_val đang là 85%, val cần chiếm 15/85 = 0.1765 của train_val.\nval_ratio_from_train_val = 0.15 / 0.85\n\nrsna_train, rsna_val = train_test_split(\n    rsna_train_val,\n    test_size=val_ratio_from_train_val,\n    random_state=SEED,\n    stratify=rsna_train_val[\"label\"]\n)\n\nprint(\"RSNA split sizes:\")\nprint(\"Train:\", len(rsna_train))\nprint(\"Val  :\", len(rsna_val))\nprint(\"Test :\", len(rsna_test))\n\nprint(\"\\nLabel distribution:\")\nprint(\"Train:\\n\", rsna_train[\"label\"].value_counts(normalize=True))\nprint(\"Val:\\n\", rsna_val[\"label\"].value_counts(normalize=True))\nprint(\"Test:\\n\", rsna_test[\"label\"].value_counts(normalize=True))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 10: LƯU METADATA RSNA\n# ============================================================\n\nrsna_train_path = OUTPUT_DIR / \"rsna_train.csv\"\nrsna_val_path = OUTPUT_DIR / \"rsna_val.csv\"\nrsna_test_path = OUTPUT_DIR / \"rsna_test.csv\"\n\nrsna_train.to_csv(rsna_train_path, index=False)\nrsna_val.to_csv(rsna_val_path, index=False)\nrsna_test.to_csv(rsna_test_path, index=False)\n\nprint(\"Saved:\")\nprint(rsna_train_path)\nprint(rsna_val_path)\nprint(rsna_test_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 11: TÌM TOÀN BỘ ẢNH NIH CHO PRETRAINING\n# ============================================================\n\nimage_exts = [\".png\", \".jpg\", \".jpeg\"]\n\nnih_image_paths = []\nfor ext in image_exts:\n    nih_image_paths.extend(list(Path(NIH_DIR).rglob(f\"*{ext}\")))\n\nnih_image_paths = sorted([str(p) for p in nih_image_paths])\n\nprint(\"Số ảnh NIH tìm được:\", len(nih_image_paths))\nprint(\"Ví dụ 5 ảnh đầu:\")\nfor p in nih_image_paths[:5]:\n    print(p)\n\nassert len(nih_image_paths) > 0, \"Không tìm thấy ảnh NIH. Hãy kiểm tra cấu trúc dataset.\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 12: TẠO NIH METADATA 30K / 50K\n# ============================================================\n\nnih_meta = pd.DataFrame({\n    \"image_path\": nih_image_paths\n})\n\n# Shuffle cố định\nnih_meta = nih_meta.sample(frac=1.0, random_state=SEED).reset_index(drop=True)\n\nn_30k = min(30000, len(nih_meta))\nn_50k = min(50000, len(nih_meta))\n\nnih_30k = nih_meta.iloc[:n_30k].copy()\nnih_50k = nih_meta.iloc[:n_50k].copy()\n\nnih_30k_path = OUTPUT_DIR / \"nih_pretrain_30k.csv\"\nnih_50k_path = OUTPUT_DIR / \"nih_pretrain_50k.csv\"\n\nnih_30k.to_csv(nih_30k_path, index=False)\nnih_50k.to_csv(nih_50k_path, index=False)\n\nprint(\"NIH total images:\", len(nih_meta))\nprint(\"Saved 30k subset:\", nih_30k_path, \"Rows:\", len(nih_30k))\nprint(\"Saved 50k subset:\", nih_50k_path, \"Rows:\", len(nih_50k))\n\ndisplay(nih_50k.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 13: KIỂM TRA ẢNH NIH CÓ ĐỌC ĐƯỢC KHÔNG\n# ============================================================\n\nsample_nih_paths = nih_50k[\"image_path\"].sample(\n    n=min(5, len(nih_50k)), \n    random_state=SEED\n).tolist()\n\nfor p in sample_nih_paths:\n    img = Image.open(p)\n    print(Path(p).name, \"mode:\", img.mode, \"size:\", img.size)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 14: TÌM FILE NHÃN VÀ THƯ MỤC ẢNH VINDR/VINBIGDATA\n# ============================================================\n\nVINDR_TRAIN_CSV = find_file(VINDR_DIR, \"train.csv\")\nVINDR_TRAIN_IMG_DIR = find_dir(VINDR_DIR, \"train\")\n\nprint(\"VINDR_TRAIN_CSV    :\", VINDR_TRAIN_CSV)\nprint(\"VINDR_TRAIN_IMG_DIR:\", VINDR_TRAIN_IMG_DIR)\n\nassert VINDR_TRAIN_CSV is not None, \"Không tìm thấy train.csv của VinDr/VinBigData\"\nassert VINDR_TRAIN_IMG_DIR is not None, \"Không tìm thấy thư mục train của VinDr/VinBigData\"\n\nprint(\"VinDr paths OK.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 15: ĐỌC NHÃN VINDR/VINBIGDATA\n# ============================================================\n\nvindr_df = pd.read_csv(VINDR_TRAIN_CSV)\n\nprint(\"Shape vindr_df:\", vindr_df.shape)\ndisplay(vindr_df.head())\n\nprint(\"\\nCác cột trong VinDr:\")\nprint(vindr_df.columns.tolist())\n\nprint(\"\\nPhân bố class_name:\")\nprint(vindr_df[\"class_name\"].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 16: MAPPING VINDR SANG BINARY PNEUMONIA / NO FINDING\n# FIX: VinDr không có class \"Pneumonia\" — dùng findings tương đương\n# ============================================================\n\nvindr_df[\"class_name_clean\"] = vindr_df[\"class_name\"].astype(str).str.strip()\n\n# FIX: Pneumonia findings trong VinDr\n# \"Lung Opacity\" + \"Consolidation\" + \"Infiltration\" là các biểu hiện\n# X-quang của viêm phổi được radiologist VinDr dùng\nPNEUMONIA_EQUIV = {\"Lung Opacity\", \"Consolidation\", \"Infiltration\"}\n\npneumonia_ids = set(\n    vindr_df.loc[\n        vindr_df[\"class_name_clean\"].isin(PNEUMONIA_EQUIV), \"image_id\"\n    ].unique()\n)\n\n# Chỉ lấy \"No finding\" thuần túy — không overlap với pneumonia\nno_finding_ids = set(\n    vindr_df.loc[\n        vindr_df[\"class_name_clean\"] == \"No finding\", \"image_id\"\n    ].unique()\n) - pneumonia_ids\n\nprint(f\"Pneumonia IDs : {len(pneumonia_ids):,}\")\nprint(f\"No finding IDs: {len(no_finding_ids):,}\")\n\n# Phần còn lại giữ nguyên\nvindr_pos = pd.DataFrame({\"image_id\": list(pneumonia_ids), \"label\": 1})\nvindr_neg = pd.DataFrame({\"image_id\": list(no_finding_ids), \"label\": 0})\n\nvindr_external_meta = pd.concat([vindr_pos, vindr_neg], ignore_index=True)\n\ndef find_vindr_image_path(image_id):\n    possible_exts = [\".dicom\", \".dcm\", \".png\", \".jpg\", \".jpeg\"]\n    for ext in possible_exts:\n        p = VINDR_TRAIN_IMG_DIR / f\"{image_id}{ext}\"\n        if p.exists():\n            return str(p)\n    return None\n\nvindr_external_meta[\"image_path\"] = vindr_external_meta[\"image_id\"].apply(find_vindr_image_path)\nvindr_external_meta = vindr_external_meta[vindr_external_meta[\"image_path\"].notna()].copy()\nvindr_external_meta = vindr_external_meta.sample(frac=1.0, random_state=SEED).reset_index(drop=True)\n\nprint(f\"\\nVinDr external binary size: {len(vindr_external_meta):,}\")\nprint(vindr_external_meta[\"label\"].value_counts())\ndisplay(vindr_external_meta.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-17T00:40:48.476153Z","iopub.execute_input":"2026-05-17T00:40:48.476503Z","iopub.status.idle":"2026-05-17T00:40:48.495833Z","shell.execute_reply.started":"2026-05-17T00:40:48.476474Z","shell.execute_reply":"2026-05-17T00:40:48.494501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 17: LƯU METADATA VINDR EXTERNAL VALIDATION\n# ============================================================\n\nvindr_external_path = OUTPUT_DIR / \"vindr_external_test.csv\"\nvindr_external_meta.to_csv(vindr_external_path, index=False)\n\nprint(\"Saved:\", vindr_external_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 18: TẠO BẢNG THỐNG KÊ DATASET\n# ============================================================\n\nstats = []\n\n# NIH\nstats.append({\n    \"dataset\": \"NIH ChestX-ray14 224x224\",\n    \"role\": \"Self-supervised pretraining\",\n    \"split\": \"pretrain_30k\",\n    \"num_images\": len(nih_30k),\n    \"num_positive\": None,\n    \"num_negative\": None,\n    \"note\": \"Unlabeled images for I-JEPA pretraining\"\n})\n\nstats.append({\n    \"dataset\": \"NIH ChestX-ray14 224x224\",\n    \"role\": \"Self-supervised pretraining\",\n    \"split\": \"pretrain_50k\",\n    \"num_images\": len(nih_50k),\n    \"num_positive\": None,\n    \"num_negative\": None,\n    \"note\": \"Main pretraining subset\"\n})\n\n# RSNA\nfor split_name, df in [\n    (\"train\", rsna_train),\n    (\"val\", rsna_val),\n    (\"test\", rsna_test)\n]:\n    stats.append({\n        \"dataset\": \"RSNA Pneumonia Detection\",\n        \"role\": \"Fine-tuning / internal evaluation\",\n        \"split\": split_name,\n        \"num_images\": len(df),\n        \"num_positive\": int((df[\"label\"] == 1).sum()),\n        \"num_negative\": int((df[\"label\"] == 0).sum()),\n        \"note\": \"Binary pneumonia classification\"\n    })\n\n# VinDr\nstats.append({\n    \"dataset\": \"VinBigData/VinDr-CXR\",\n    \"role\": \"External validation\",\n    \"split\": \"external_test\",\n    \"num_images\": len(vindr_external_meta),\n    \"num_positive\": int((vindr_external_meta[\"label\"] == 1).sum()),\n    \"num_negative\": int((vindr_external_meta[\"label\"] == 0).sum()),\n    \"note\": \"Only Pneumonia and No finding images are used\"\n})\n\ndataset_statistics = pd.DataFrame(stats)\nstats_path = OUTPUT_DIR / \"dataset_statistics.csv\"\ndataset_statistics.to_csv(stats_path, index=False)\n\ndisplay(dataset_statistics)\n\nprint(\"Saved:\", stats_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 19: KIỂM TRA FILE OUTPUT\n# ============================================================\n\nprint(\"Các file đã tạo trong OUTPUT_DIR:\")\nfor p in sorted(OUTPUT_DIR.glob(\"*\")):\n    print(\"-\", p.name)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CELL 20: NÉN METADATA THÀNH ZIP\n# ============================================================\n\nzip_path = \"/kaggle/working/metadata_notebook01\"\nshutil.make_archive(zip_path, \"zip\", OUTPUT_DIR)\n\nprint(\"Created zip:\", zip_path + \".zip\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}