{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.0"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":10338,"databundleVersionId":862042,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":317003019,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":318894100,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":319266295,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":319949819,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":320520534,"isSourceIdPinned":false}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"cf330b2d-bc20-47b4-b93b-7c2d078c0478","cell_type":"markdown","source":"# Notebook 09 — CLAHE Ablation Study\n\n**Mục tiêu:** So sánh hiệu năng có/không CLAHE preprocessing trên tất cả 7 models.\n\n**Models:** Linear Probe · Partial FT v1/v2 · Full FT v1/v2 · ViT-Small · ResNet50\n\n**Strategy:** Load existing predictions (no-CLAHE) từ CSV → chỉ chạy CLAHE inference mới → compare.\n\n**Input cần add:**\n- Output NB01 → `rsna_test.csv`\n- Output NB04 v1 → checkpoints Partial v1, Full v1, Linear Probe\n- Output NB04 v3 → checkpoints Partial v2, Full v2\n- Output NB04/05 → prediction CSVs (no-CLAHE baseline)\n- Output NB04b → `ijepa_full_finetune_resume_best.pth`\n- Output NB02 → ResNet50 + ViT checkpoints\n- RSNA dataset → ảnh DICOM","metadata":{}},{"id":"e77c73d3-7f6a-4a2a-b536-49c21605b8cc","cell_type":"code","source":"# ============================================================\n# CELL 1: IMPORTS + CONFIG\n# ============================================================\nimport os, gc, json, math, time, shutil, warnings\nfrom pathlib import Path\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as mpatches\nfrom tqdm.auto import tqdm\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nimport torchvision.models as tvm\n\ntry:\n    import timm\nexcept ImportError:\n    import subprocess; subprocess.run([\"pip\",\"install\",\"-q\",\"timm\"])\n    import timm\n\ntry:\n    import pydicom\nexcept ImportError:\n    import subprocess; subprocess.run([\"pip\",\"install\",\"-q\",\"pydicom\"])\n    import pydicom\n\ntry:\n    import cv2\nexcept ImportError:\n    import subprocess; subprocess.run([\"pip\",\"install\",\"-q\",\"opencv-python-headless\"])\n    import cv2\n\nfrom sklearn.metrics import roc_auc_score, f1_score, recall_score, precision_score, confusion_matrix\n\nSEED = 42\nimport random\nrandom.seed(SEED); np.random.seed(SEED)\ntorch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))\n\nOUTPUT_DIR = Path(\"/kaggle/working/notebook09_clahe_ablation\")\nTABLE_DIR  = OUTPUT_DIR / \"tables\"\nFIG_DIR    = OUTPUT_DIR / \"figures\"\nfor d in [TABLE_DIR, FIG_DIR]: d.mkdir(parents=True, exist_ok=True)\n\nINPUT_ROOT   = Path(\"/kaggle/input\")\nWORKING_ROOT = Path(\"/kaggle/working\")\nIMG_SIZE     = 224\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\n\n# CLAHE params — standard for chest X-ray\nCLAHE_CLIP  = 2.0\nCLAHE_GRID  = (8, 8)\n\nSESSION_SAFE_SECONDS = 11.0 * 3600\nNOTEBOOK_START       = time.time()\nprint(\"Output:\", OUTPUT_DIR)","metadata":{},"outputs":[],"execution_count":null},{"id":"6d3a604b-5d69-4ce7-abdd-49be336c1b10","cell_type":"code","source":"# ============================================================\n# CELL 2: TÌM INPUT FILES\n# ============================================================\ndef find_file(name):\n    for root in [WORKING_ROOT, INPUT_ROOT]:\n        for p in root.rglob(name):\n            return p\n    return None\n\n# ── RSNA metadata ────────────────────────────────────────\nRSNA_TEST_CSV = find_file(\"rsna_test.csv\")\nRSNA_TRAIN_CSV = find_file(\"rsna_train.csv\")\n\n# ── Checkpoints ──────────────────────────────────────────\nCKPT = {\n    \"I-JEPA Linear Probe\":    find_file(\"ijepa_linear_probe_best.pth\"),\n    \"I-JEPA Partial FT v1\":   find_file(\"ijepa_partial_finetune_best.pth\"),\n    \"I-JEPA Partial FT v2\":   find_file(\"ijepa_partial_finetune_v2_best.pth\"),\n    \"I-JEPA Full FT v1\":      find_file(\"ijepa_full_finetune_resume_best.pth\"),\n    \"I-JEPA Full FT v2\":      find_file(\"ijepa_full_finetune_v2_best.pth\"),\n    \"ResNet50 ImageNet\":       find_file(\"resnet50_imagenet_best.pth\"),\n    \"ViT-Small ImageNet\":      find_file(\"vit_imagenet_best.pth\"),\n}\n\n# ── Existing predictions (no-CLAHE baseline) ─────────────\nPRED_CSV = {\n    \"I-JEPA Linear Probe\":    find_file(\"ijepa_linear_probe_predictions.csv\"),\n    \"I-JEPA Partial FT v1\":   find_file(\"ijepa_partial_finetune_predictions.csv\"),\n    \"I-JEPA Partial FT v2\":   find_file(\"ijepa_partial_finetune_v2_predictions.csv\"),\n    \"I-JEPA Full FT v1\":      find_file(\"ijepa_full_ft_resume_predictions.csv\"),\n    \"I-JEPA Full FT v2\":      find_file(\"ijepa_full_finetune_v2_predictions.csv\"),\n    \"ResNet50 ImageNet\":       find_file(\"resnet50_predictions.csv\"),\n    \"ViT-Small ImageNet\":      find_file(\"vit_predictions.csv\"),\n}\n\nprint(\"=== Checkpoints ===\")\nfor k, v in CKPT.items():\n    print(f\"  {'✓' if v else '✗':2s} {k}\")\n\nprint(\"\\n=== Prediction CSVs (no-CLAHE baseline) ===\")\nfor k, v in PRED_CSV.items():\n    print(f\"  {'✓' if v else '○':2s} {k}  {'← will generate' if not v else ''}\")\n\nassert RSNA_TEST_CSV, \"Thiếu rsna_test.csv\"\nprint(\"\\nRSNA test CSV:\", RSNA_TEST_CSV)","metadata":{},"outputs":[],"execution_count":null},{"id":"41b7f337-4ed2-4931-8653-240bf9ab81a7","cell_type":"code","source":"# ============================================================\n# CELL 3: UTILITY FUNCTIONS\n# ============================================================\n\n# ── Path fix ──────────────────────────────────────────────\ndef fix_df_paths(df):\n    def _fix(p):\n        if Path(p).exists(): return p\n        m = list(INPUT_ROOT.rglob(Path(p).name))\n        return str(m[0]) if m else None\n    df = df.copy()\n    df[\"image_path\"] = df[\"image_path\"].apply(_fix)\n    return df.dropna(subset=[\"image_path\"]).reset_index(drop=True)\n\n# ── CLAHE helper ──────────────────────────────────────────\n_clahe_obj = cv2.createCLAHE(clipLimit=CLAHE_CLIP, tileGridSize=CLAHE_GRID)\n\ndef apply_clahe(gray_uint8: np.ndarray) -> np.ndarray:\n    \"\"\"Input: uint8 grayscale [H,W]. Output: uint8 CLAHE-enhanced [H,W].\"\"\"\n    return _clahe_obj.apply(gray_uint8)\n\n# ── Read image — với và không CLAHE ──────────────────────\ndef read_image(path, use_clahe=False):\n    p = str(path)\n    if p.endswith(\".dcm\"):\n        arr = pydicom.dcmread(p).pixel_array.astype(np.float32)\n        # MONOCHROME1 fix\n        ds = pydicom.dcmread(p)\n        if getattr(ds, \"PhotometricInterpretation\",\"\") == \"MONOCHROME1\":\n            arr = arr.max() - arr\n        mn, mx = arr.min(), arr.max()\n        arr = ((arr - mn) / (mx - mn + 1e-8) * 255).astype(np.uint8)\n    else:\n        arr = np.array(Image.open(p).convert(\"L\"))\n    if use_clahe:\n        arr = apply_clahe(arr)\n    return Image.fromarray(arr).convert(\"RGB\")\n\n# ── Transforms ─────────────────────────────────────────\ndef make_eval_transform():\n    return T.Compose([\n        T.Resize((IMG_SIZE, IMG_SIZE)),\n        T.ToTensor(),\n        T.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n    ])\n\n# ── Dataset ────────────────────────────────────────────\nclass RSNADataset(Dataset):\n    def __init__(self, df, use_clahe=False, transform=None):\n        self.df         = df.reset_index(drop=True)\n        self.use_clahe  = use_clahe\n        self.transform  = transform or make_eval_transform()\n    def __len__(self): return len(self.df)\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img = read_image(row[\"image_path\"], use_clahe=self.use_clahe)\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(float(row[\"label\"]), dtype=torch.float32)\n\n# ── Metrics ─────────────────────────────────────────────\ndef compute_metrics(y_true, y_prob, threshold=0.5):\n    y_true = np.array(y_true).astype(int)\n    y_prob = np.array(y_prob).astype(float)\n    y_pred = (y_prob >= threshold).astype(int)\n    try:    auc = roc_auc_score(y_true, y_prob)\n    except: auc = np.nan\n    tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0,1]).ravel()\n    return {\n        \"auc\":              float(auc),\n        \"f1\":               float(f1_score(y_true, y_pred, zero_division=0)),\n        \"recall_pneumonia\": float(recall_score(y_true, y_pred, zero_division=0)),\n        \"precision\":        float(precision_score(y_true, y_pred, zero_division=0)),\n        \"specificity\":      float(tn / (tn + fp + 1e-8)),\n    }\n\nprint(\"Utilities ready ✓\")\nprint(f\"CLAHE: clipLimit={CLAHE_CLIP}, tileGridSize={CLAHE_GRID}\")","metadata":{},"outputs":[],"execution_count":null},{"id":"5f9e7db2-73b6-4d2a-a386-cfce78c1a63b","cell_type":"code","source":"# ============================================================\n# CELL 4: LOAD TEST DATA + POS_WEIGHT\n# ============================================================\ntest_df  = fix_df_paths(pd.read_csv(RSNA_TEST_CSV))\n\n# POS_WEIGHT từ train set nếu có, fallback hardcode\nif RSNA_TRAIN_CSV:\n    train_df = fix_df_paths(pd.read_csv(RSNA_TRAIN_CSV))\n    n_neg = (train_df[\"label\"]==0).sum()\n    n_pos = (train_df[\"label\"]==1).sum()\n    POS_WEIGHT = torch.tensor([n_neg/n_pos], dtype=torch.float32).to(DEVICE)\nelse:\n    POS_WEIGHT = torch.tensor([3.439], dtype=torch.float32).to(DEVICE)  # fallback\n\nprint(f\"Test: {len(test_df):,} samples | \"\n      f\"pos={test_df['label'].sum()} ({test_df['label'].mean()*100:.1f}%)\")\nprint(f\"POS_WEIGHT = {POS_WEIGHT.item():.4f}\")\n\n# Verify CLAHE effect on 1 sample\n_sample_path = test_df[\"image_path\"].iloc[0]\nimg_raw   = read_image(_sample_path, use_clahe=False)\nimg_clahe = read_image(_sample_path, use_clahe=True)\narr_raw   = np.array(img_raw.convert(\"L\"))\narr_clahe = np.array(img_clahe.convert(\"L\"))\nprint(f\"\\nCLAHE effect on sample image:\")\nprint(f\"  No CLAHE: mean={arr_raw.mean():.1f}, std={arr_raw.std():.1f}\")\nprint(f\"  CLAHE:    mean={arr_clahe.mean():.1f}, std={arr_clahe.std():.1f}\")\nprint(f\"  Pixel diff: {np.abs(arr_raw.astype(int)-arr_clahe.astype(int)).mean():.2f} avg\")","metadata":{},"outputs":[],"execution_count":null},{"id":"a09c70d6-e7e3-4093-a0e7-592cd0dfc943","cell_type":"code","source":"# ============================================================\n# CELL 5: MODEL LOADERS\n# ============================================================\n\nclass IJEPAClassifier(nn.Module):\n    def __init__(self, encoder, embed_dim=384, dropout=0.1):\n        super().__init__()\n        self.encoder    = encoder\n        self.classifier = nn.Sequential(\n            nn.LayerNorm(embed_dim), nn.Dropout(dropout), nn.Linear(embed_dim, 1)\n        )\n    def forward(self, x):\n        return self.classifier(self.encoder(x)).squeeze(-1)\n\ndef load_ijepa(ckpt_path, dropout=0.1):\n    enc = timm.create_model(\"vit_small_patch16_224\", pretrained=False, num_classes=0)\n    model = IJEPAClassifier(enc, embed_dim=enc.num_features, dropout=dropout)\n    try:    ck = torch.load(ckpt_path, map_location=DEVICE, weights_only=False)\n    except: ck = torch.load(ckpt_path, map_location=DEVICE)\n    model.load_state_dict(ck[\"model_state_dict\"], strict=False)\n    return model.to(DEVICE)\n\ndef load_resnet50(ckpt_path):\n    model = tvm.resnet50(weights=None)\n    model.fc = nn.Linear(model.fc.in_features, 1)\n    try:    ck = torch.load(ckpt_path, map_location=DEVICE, weights_only=False)\n    except: ck = torch.load(ckpt_path, map_location=DEVICE)\n    sd = ck.get(\"model_state_dict\", ck)\n    model.load_state_dict(sd, strict=False)\n    return model.to(DEVICE)\n\ndef load_vit(ckpt_path):\n    model = timm.create_model(\"vit_small_patch16_224\", pretrained=False, num_classes=1)\n    try:    ck = torch.load(ckpt_path, map_location=DEVICE, weights_only=False)\n    except: ck = torch.load(ckpt_path, map_location=DEVICE)\n    sd = ck.get(\"model_state_dict\", ck)\n    model.load_state_dict(sd, strict=False)\n    return model.to(DEVICE)\n\n# Map model name → loader function + dropout (for IJEPA)\nMODEL_LOADERS = {\n    \"I-JEPA Linear Probe\":  (load_ijepa,    {\"dropout\": 0.2}),\n    \"I-JEPA Partial FT v1\": (load_ijepa,    {\"dropout\": 0.2}),\n    \"I-JEPA Partial FT v2\": (load_ijepa,    {\"dropout\": 0.1}),\n    \"I-JEPA Full FT v1\":    (load_ijepa,    {\"dropout\": 0.2}),\n    \"I-JEPA Full FT v2\":    (load_ijepa,    {\"dropout\": 0.1}),\n    \"ResNet50 ImageNet\":    (load_resnet50, {}),\n    \"ViT-Small ImageNet\":   (load_vit,      {}),\n}\n\nprint(\"Model loaders ready ✓\")","metadata":{},"outputs":[],"execution_count":null},{"id":"0d6b4536-bb9d-4c0e-94aa-b51c51430929","cell_type":"code","source":"# ============================================================\n# CELL 6: LOAD NO-CLAHE BASELINE TỪ CSV HOẶC GENERATE\n# ============================================================\nbaseline_preds = {}   # model_name → DataFrame(label, prob_pneumonia)\n\nfor model_name, csv_path in PRED_CSV.items():\n    if csv_path:\n        df = pd.read_csv(csv_path)\n        df[\"label\"]          = df[\"label\"].astype(int)\n        df[\"prob_pneumonia\"] = df[\"prob_pneumonia\"].astype(float)\n        baseline_preds[model_name] = df\n        auc = roc_auc_score(df[\"label\"], df[\"prob_pneumonia\"])\n        print(f\"  ✓ {model_name}: loaded {len(df):,} | AUC={auc:.4f}\")\n    else:\n        print(f\"  ○ {model_name}: CSV not found — will generate from checkpoint\")\n\n# Generate from checkpoint nếu thiếu CSV\n@torch.no_grad()\ndef run_inference(model, df, use_clahe=False, desc=\"infer\"):\n    model.eval()\n    loader = DataLoader(RSNADataset(df, use_clahe=use_clahe),\n                        batch_size=16, shuffle=False, num_workers=2, pin_memory=True)\n    all_probs, all_labels = [], []\n    for imgs, labels in tqdm(loader, desc=desc, leave=False):\n        imgs = imgs.to(DEVICE)\n        with torch.amp.autocast(\"cuda\", enabled=torch.cuda.is_available()):\n            logits = model(imgs)\n        logits = logits.squeeze(-1) if logits.dim() > 1 else logits\n        all_probs.extend(torch.sigmoid(logits).cpu().numpy())\n        all_labels.extend(labels.numpy())\n    return pd.DataFrame({\"label\": [int(l) for l in all_labels],\n                         \"prob_pneumonia\": all_probs})\n\nfor model_name in PRED_CSV:\n    if model_name in baseline_preds:\n        continue\n    ckpt = CKPT.get(model_name)\n    if not ckpt:\n        print(f\"  ✗ {model_name}: no checkpoint — skip\")\n        continue\n    if time.time() - NOTEBOOK_START > SESSION_SAFE_SECONDS:\n        print(\"⏱  Time guard. Stop.\"); break\n\n    loader_fn, kwargs = MODEL_LOADERS[model_name]\n    model = loader_fn(ckpt, **kwargs)\n    pred_df = run_inference(model, test_df, use_clahe=False, desc=f\"{model_name} (no-CLAHE)\")\n    auc = roc_auc_score(pred_df[\"label\"], pred_df[\"prob_pneumonia\"])\n    baseline_preds[model_name] = pred_df\n    pred_df.to_csv(OUTPUT_DIR / f\"{model_name.lower().replace(' ','_')}_no_clahe.csv\", index=False)\n    print(f\"  ✓ {model_name}: generated | AUC={auc:.4f}\")\n    del model; gc.collect()\n    if torch.cuda.is_available(): torch.cuda.empty_cache()\n\nprint(f\"\\nBaseline (no-CLAHE): {len(baseline_preds)} models loaded\")","metadata":{},"outputs":[],"execution_count":null},{"id":"e2cc799e-aa7e-4f0c-ad6c-99ee47f23e4d","cell_type":"code","source":"# ============================================================\n# CELL 7: CHẠY CLAHE INFERENCE CHO TẤT CẢ MODELS\n# ============================================================\nclahe_preds = {}\n\nfor model_name, ckpt in CKPT.items():\n    if time.time() - NOTEBOOK_START > SESSION_SAFE_SECONDS:\n        print(f\"⏱  Time guard trước {model_name}. Dừng.\"); break\n\n    if not ckpt:\n        print(f\"  ✗ {model_name}: no checkpoint — skip\")\n        continue\n    if model_name not in baseline_preds:\n        print(f\"  ○ {model_name}: no baseline — skip CLAHE too\")\n        continue\n\n    print(f\"  Running CLAHE: {model_name}\")\n    loader_fn, kwargs = MODEL_LOADERS[model_name]\n    model = loader_fn(ckpt, **kwargs)\n\n    pred_df = run_inference(model, test_df, use_clahe=True,\n                             desc=f\"{model_name} (CLAHE)\")\n    auc = roc_auc_score(pred_df[\"label\"], pred_df[\"prob_pneumonia\"])\n    clahe_preds[model_name] = pred_df\n    pred_df.to_csv(OUTPUT_DIR / f\"{model_name.lower().replace(' ','_')}_clahe.csv\", index=False)\n    print(f\"    AUC={auc:.4f}\")\n\n    del model; gc.collect()\n    if torch.cuda.is_available(): torch.cuda.empty_cache()\n\nprint(f\"\\nCLAHE inference: {len(clahe_preds)} models done\")","metadata":{},"outputs":[],"execution_count":null},{"id":"7d924377-6972-4cd9-85b3-25051b681a5e","cell_type":"code","source":"# ============================================================\n# CELL 8: TỔNG HỢP KẾT QUẢ + BẢNG SO SÁNH\n# ============================================================\nMODEL_ORDER = [\n    \"I-JEPA Linear Probe\",\n    \"I-JEPA Partial FT v1\", \"I-JEPA Partial FT v2\",\n    \"I-JEPA Full FT v1\",    \"I-JEPA Full FT v2\",\n    \"ResNet50 ImageNet\",    \"ViT-Small ImageNet\",\n]\n\nrows = []\nfor model_name in MODEL_ORDER:\n    base_df  = baseline_preds.get(model_name)\n    clahe_df = clahe_preds.get(model_name)\n\n    if base_df is None and clahe_df is None:\n        continue\n\n    row = {\"model\": model_name}\n\n    if base_df is not None:\n        m_base = compute_metrics(base_df[\"label\"], base_df[\"prob_pneumonia\"])\n        row.update({f\"no_clahe_{k}\": v for k, v in m_base.items()})\n    else:\n        m_base = None\n\n    if clahe_df is not None:\n        m_clahe = compute_metrics(clahe_df[\"label\"], clahe_df[\"prob_pneumonia\"])\n        row.update({f\"clahe_{k}\": v for k, v in m_clahe.items()})\n    else:\n        m_clahe = None\n\n    if m_base and m_clahe:\n        row[\"delta_auc\"]    = round(m_clahe[\"auc\"]              - m_base[\"auc\"],              4)\n        row[\"delta_f1\"]     = round(m_clahe[\"f1\"]               - m_base[\"f1\"],               4)\n        row[\"delta_recall\"] = round(m_clahe[\"recall_pneumonia\"]  - m_base[\"recall_pneumonia\"], 4)\n\n    rows.append(row)\n\nresults_df = pd.DataFrame(rows)\nresults_df.to_csv(TABLE_DIR / \"clahe_ablation_full.csv\", index=False)\n\n# Bảng tóm tắt để display\ncols_show = [\"model\",\n             \"no_clahe_auc\", \"clahe_auc\",    \"delta_auc\",\n             \"no_clahe_f1\",  \"clahe_f1\",     \"delta_f1\",\n             \"no_clahe_recall_pneumonia\", \"clahe_recall_pneumonia\", \"delta_recall\"]\nsummary_df = results_df[[c for c in cols_show if c in results_df.columns]].round(4)\nsummary_df.to_csv(TABLE_DIR / \"clahe_ablation_summary.csv\", index=False)\n\nprint(\"=== CLAHE Ablation Summary ===\")\ndisplay(summary_df)","metadata":{},"outputs":[],"execution_count":null},{"id":"a8f7de8f-9e56-42cf-a067-a2599e2a94fa","cell_type":"code","source":"# ============================================================\n# CELL 9: VISUALIZE — BAR CHART + DELTA CHART\n# ============================================================\nmodels_plot  = [r[\"model\"] for r in rows if \"delta_auc\" in r]\nlabels_short = [m.replace(\"I-JEPA \",\"\").replace(\" ImageNet\",\"\") for m in models_plot]\n\nno_clahe_auc = [results_df.loc[results_df.model==m, \"no_clahe_auc\"].values[0] for m in models_plot]\nclahe_auc    = [results_df.loc[results_df.model==m, \"clahe_auc\"].values[0]    for m in models_plot]\ndelta_auc    = [results_df.loc[results_df.model==m, \"delta_auc\"].values[0]    for m in models_plot]\n\nno_clahe_recall = [results_df.loc[results_df.model==m, \"no_clahe_recall_pneumonia\"].values[0] for m in models_plot]\nclahe_recall    = [results_df.loc[results_df.model==m, \"clahe_recall_pneumonia\"].values[0]    for m in models_plot]\ndelta_recall    = [results_df.loc[results_df.model==m, \"delta_recall\"].values[0]              for m in models_plot]\n\nx     = np.arange(len(models_plot))\nwidth = 0.38\n\nfig, axes = plt.subplots(2, 2, figsize=(16, 10))\nfig.suptitle(\"CLAHE Ablation Study — RSNA Test Set\", fontsize=14, fontweight=\"bold\", y=0.98)\n\n# ── AUC comparison ────────────────────────────────────────\nax = axes[0,0]\nb1 = ax.bar(x - width/2, no_clahe_auc, width, label=\"No CLAHE\",\n            color=\"#5b8ac7\", alpha=0.85, edgecolor=\"white\")\nb2 = ax.bar(x + width/2, clahe_auc,    width, label=\"CLAHE\",\n            color=\"#e8954a\", alpha=0.85, edgecolor=\"white\")\nax.set_xticks(x); ax.set_xticklabels(labels_short, fontsize=8, rotation=20, ha=\"right\")\nax.set_ylabel(\"AUC\"); ax.set_title(\"AUC: No CLAHE vs CLAHE\", fontweight=\"bold\")\nax.set_ylim(min(no_clahe_auc + clahe_auc) * 0.97, max(no_clahe_auc + clahe_auc) * 1.02)\nax.legend(fontsize=9); ax.grid(axis=\"y\", alpha=0.3)\nfor bar, val in zip(b2, clahe_auc):\n    ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.001,\n            f\"{val:.3f}\", ha=\"center\", va=\"bottom\", fontsize=7)\n\n# ── Delta AUC ─────────────────────────────────────────────\nax = axes[0,1]\ncolors_delta = [\"#1D9E75\" if d >= 0 else \"#E24B4A\" for d in delta_auc]\nbars = ax.bar(x, delta_auc, color=colors_delta, edgecolor=\"white\", alpha=0.85)\nax.axhline(0, color=\"black\", linewidth=0.8, linestyle=\"--\")\nax.set_xticks(x); ax.set_xticklabels(labels_short, fontsize=8, rotation=20, ha=\"right\")\nax.set_ylabel(\"ΔAUC (CLAHE − No CLAHE)\")\nax.set_title(\"Delta AUC — dương là CLAHE tốt hơn\", fontweight=\"bold\")\nax.grid(axis=\"y\", alpha=0.3)\nfor bar, val in zip(bars, delta_auc):\n    ypos = bar.get_height() + 0.0005 if val >= 0 else bar.get_height() - 0.001\n    ax.text(bar.get_x()+bar.get_width()/2, ypos,\n            f\"{val:+.4f}\", ha=\"center\", va=\"bottom\" if val >= 0 else \"top\",\n            fontsize=8, fontweight=\"bold\")\n\n# ── Recall comparison ─────────────────────────────────────\nax = axes[1,0]\nax.bar(x - width/2, no_clahe_recall, width, label=\"No CLAHE\",\n       color=\"#5b8ac7\", alpha=0.85, edgecolor=\"white\")\nax.bar(x + width/2, clahe_recall,    width, label=\"CLAHE\",\n       color=\"#e8954a\", alpha=0.85, edgecolor=\"white\")\nax.set_xticks(x); ax.set_xticklabels(labels_short, fontsize=8, rotation=20, ha=\"right\")\nax.set_ylabel(\"Recall Pneumonia\"); ax.set_title(\"Recall: No CLAHE vs CLAHE\", fontweight=\"bold\")\nax.set_ylim(min(no_clahe_recall+clahe_recall)*0.95, min(1.0, max(no_clahe_recall+clahe_recall)*1.05))\nax.legend(fontsize=9); ax.grid(axis=\"y\", alpha=0.3)\n\n# ── Delta Recall ──────────────────────────────────────────\nax = axes[1,1]\ncolors_dr = [\"#1D9E75\" if d >= 0 else \"#E24B4A\" for d in delta_recall]\nbars = ax.bar(x, delta_recall, color=colors_dr, edgecolor=\"white\", alpha=0.85)\nax.axhline(0, color=\"black\", linewidth=0.8, linestyle=\"--\")\nax.set_xticks(x); ax.set_xticklabels(labels_short, fontsize=8, rotation=20, ha=\"right\")\nax.set_ylabel(\"ΔRecall\")\nax.set_title(\"Delta Recall — dương là CLAHE tốt hơn\", fontweight=\"bold\")\nax.grid(axis=\"y\", alpha=0.3)\nfor bar, val in zip(bars, delta_recall):\n    ypos = bar.get_height() + 0.001 if val >= 0 else bar.get_height() - 0.002\n    ax.text(bar.get_x()+bar.get_width()/2, ypos,\n            f\"{val:+.4f}\", ha=\"center\", va=\"bottom\" if val >= 0 else \"top\",\n            fontsize=8, fontweight=\"bold\")\n\nplt.tight_layout()\nfig.savefig(FIG_DIR/\"clahe_ablation.png\", dpi=180, bbox_inches=\"tight\")\nplt.show()\nprint(\"Saved: clahe_ablation.png\")","metadata":{},"outputs":[],"execution_count":null},{"id":"f2cb26fe-ccc5-4bd8-8ab4-c77dcd8acaff","cell_type":"code","source":"# ============================================================\n# CELL 10: PHÂN TÍCH VÀ NHẬN XÉT TỰ ĐỘNG\n# ============================================================\nif \"delta_auc\" in results_df.columns:\n    print(\"=== CLAHE IMPACT ANALYSIS ===\\n\")\n    for _, row in results_df.iterrows():\n        if \"delta_auc\" not in row or pd.isna(row.get(\"delta_auc\")): continue\n        d_auc    = row[\"delta_auc\"]\n        d_recall = row.get(\"delta_recall\", 0)\n        verdict  = (\"✅ CẢI THIỆN\" if d_auc > 0.005\n                    else \"❌ GIẢM\"    if d_auc < -0.005\n                    else \"➖ KHÔNG ĐỔI\")\n        print(f\"{row['model']:30s} | ΔAUC={d_auc:+.4f} ΔRecall={d_recall:+.4f} | {verdict}\")\n\n    print()\n    delta_vals = results_df[\"delta_auc\"].dropna()\n    n_pos  = (delta_vals > 0.005).sum()\n    n_neg  = (delta_vals < -0.005).sum()\n    n_neut = len(delta_vals) - n_pos - n_neg\n    print(f\"Tổng kết: {n_pos} models cải thiện | {n_neut} không đổi | {n_neg} giảm hiệu năng\")\n    print(f\"Mean ΔAUC: {delta_vals.mean():+.5f}\")\n    print(f\"Best improvement: {delta_vals.max():+.4f} ({results_df.loc[delta_vals.idxmax(),'model']})\")\n    print(f\"Worst drop:       {delta_vals.min():+.4f} ({results_df.loc[delta_vals.idxmin(),'model']})\")\n\n    if delta_vals.mean() > 0.003:\n        print(\"\\nKết luận: CLAHE có lợi nhẹ trên tập dữ liệu này.\")\n    elif delta_vals.mean() < -0.003:\n        print(\"\\nKết luận: CLAHE không cải thiện — model đã robust với contrast mặc định.\")\n    else:\n        print(\"\\nKết luận: CLAHE không tạo ra khác biệt đáng kể (|ΔAUC| < 0.003).\")\n    print(\"\\nNote: Đây là valid research finding. Cả 2 kết quả đều publishable.\")","metadata":{},"outputs":[],"execution_count":null},{"id":"a95c2601-6bfb-4bda-a023-be1741e679e2","cell_type":"code","source":"# ============================================================\n# CELL 11: LƯU KẾT QUẢ + NÉN OUTPUT\n# ============================================================\nelapsed = (time.time() - NOTEBOOK_START) / 3600\nprint(f\"Tổng thời gian: {elapsed:.2f}h\\n\")\n\nprint(\"Files trong output:\")\nfor p in sorted(OUTPUT_DIR.rglob(\"*\")):\n    if p.is_file():\n        print(f\"  {str(p.relative_to(OUTPUT_DIR)):55s} ({p.stat().st_size/1e3:.1f} KB)\")\n\nshutil.make_archive(\"/kaggle/working/notebook09_clahe_ablation\", \"zip\", OUTPUT_DIR)\nprint(\"\\nCreated: notebook09_clahe_ablation.zip\")","metadata":{},"outputs":[],"execution_count":null}]}