{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# RSNA 2025 • “Hello, data!”","metadata":{}},{"cell_type":"markdown","source":"# ================================================================\n# RSNA 2025 • STEP 1 — “Hello, data!” (no heavy imports, no SciPy)\n# ================================================================\n# ▸ What this cell does\n#   1. Pins harmless library versions (avoid NumPy/SciPy mismatches)\n#   2. Loads train.csv + train_localizers.csv\n#   3. Prints basic info so you know the data is live\n# ---------------------------------------------------------------\n\n# -------- 1. installs (fast, quiet) -----------------------------\n# !pip install -q \"polars>=0.20\" pandas==2.1.4 pylibjpeg pylibjpeg-libjpeg python-gdcm\n\n# -------- 2. standard imports ----------------------------------\nimport os, warnings, random, math, json, time\nfrom pathlib import Path\nimport polars as pl\nimport pandas as pd\n\nwarnings.filterwarnings(\"ignore\")\n\n# -------- 3. dataset paths -------------------------------------\nROOT       = Path('/kaggle/input/rsna-intracranial-aneurysm-detection')\nSERIES_DIR = ROOT / 'series'\nassert ROOT.exists(),  \"❌ RSNA dataset not found!\"\nassert SERIES_DIR.exists(), \"❌ series/ folder missing!\"\n\n# -------- 4. config: choose Polars or Pandas -------------------\nUSE_POLARS = True           # ◄ flip to False if you prefer Pandas\n\nif USE_POLARS:\n    train_df = pl.read_csv(ROOT / 'train.csv')\n    loc_df   = pl.read_csv(ROOT / 'train_localizers.csv')\nelse:\n    train_df = pd.read_csv(ROOT / 'train.csv', low_memory=False)\n    loc_df   = pd.read_csv(ROOT / 'train_localizers.csv', low_memory=False)\n\n# -------- 5. label columns -------------------------------------\nLABEL_COLS = [\n    c for c in (train_df.columns if USE_POLARS else train_df.columns.tolist())\n    if c not in ('SeriesInstanceUID', 'PatientAge', 'PatientSex', 'Modality')\n]\n\n# -------- 6. basic prints --------------------------------------\nprint(\"✅ Data loaded\")\nprint(f\"   • train rows        : {len(train_df):,}\")\nprint(f\"   • localizer rows    : {len(loc_df):,}\")\nprint(f\"   • patient modalities: {train_df.select('Modality').unique() if USE_POLARS else train_df['Modality'].unique()}\")\nprint(f\"   • label columns (14): {LABEL_COLS[:3]} … {LABEL_COLS[-3:]}\")\n\n# optional — peek at first 3 rows\nprint(\"\\ntrain_df head:\")\nprint(train_df.head(3) if USE_POLARS else train_df.head(3))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2025-08-01T06:12:13.988014Z","iopub.execute_input":"2025-08-01T06:12:13.988786Z","iopub.status.idle":"2025-08-01T06:12:23.941982Z","shell.execute_reply.started":"2025-08-01T06:12:13.98876Z","shell.execute_reply":"2025-08-01T06:12:23.941189Z"}}},{"cell_type":"markdown","source":"# Lightweight Dataset + DataLoader (RAM-safe, no SciPy)","metadata":{}},{"cell_type":"markdown","source":"# ======================================================================\n# STEP 2 · RAM-safe Dataset & DataLoader  (no SciPy, no heavy transforms)\n# ======================================================================\nimport numpy as np, torch, pydicom\nfrom pathlib import Path\nfrom functools import lru_cache\nfrom torch.utils.data import Dataset, DataLoader\n\n# ------------------------------------------------------------\n# 1 ▸ tiny cache + robust loader\n#       • keeps ≤4 volumes in memory\n#       • reads only every 4th slice → ¼ RAM / I/O\n# ------------------------------------------------------------\n@lru_cache(maxsize=4)\ndef load_volume(series_uid: str, stride: int = 4):\n    \"\"\"Return (D,H,W) volume and modality, reading only 1/stride slices.\"\"\"\n    zs, imgs = [], []\n    series_dir = SERIES_DIR / series_uid\n\n    for fp in sorted(series_dir.glob(\"*.dcm\"))[::stride]:\n        ds  = pydicom.dcmread(fp, stop_before_pixels=False)\n        z0  = float(ds.ImagePositionPatient[2]) if \"ImagePositionPatient\" in ds else float(ds.InstanceNumber)\n        arr = ds.pixel_array\n\n        if arr.ndim == 3:                       # multi-frame: iterate frames\n            spacing = float(getattr(ds, \"SliceThickness\", 1.0))\n            for i in range(arr.shape[0]):\n                zs.append(z0 + i * spacing)\n                imgs.append(arr[i])\n        else:                                   # single frame\n            zs.append(z0)\n            imgs.append(arr)\n\n    # ---- safe sort by z only -------------------------------------------\n    order = np.argsort(zs)                     # indices sorted by z\n    imgs  = [imgs[i] for i in order]\n\n    # ---- make all slices the same shape (pad with zeros) ---------------\n    max_h = max(arr.shape[0] for arr in imgs)\n    max_w = max(arr.shape[1] for arr in imgs)\n\n    padded = []\n    for arr in imgs:\n        h, w = arr.shape\n        pad_h0 = (max_h - h) // 2\n        pad_h1 = max_h - h - pad_h0\n        pad_w0 = (max_w - w) // 2\n        pad_w1 = max_w - w - pad_w0\n        arr_p  = np.pad(arr,\n                       ((pad_h0, pad_h1), (pad_w0, pad_w1)),\n                       mode='constant',\n                       constant_values=0)\n        padded.append(arr_p.astype(np.float32))\n\n    vol = np.stack(padded)                     # (D, max_h, max_w)\n    modality = ds.Modality\n    return vol, modality\n\n# ------------------------------------------------------------\n# 2 ▸ tiny normalise  (CT brain-window or MRI min-max)\n# ------------------------------------------------------------\ndef normalise(vol: np.ndarray, modality: str):\n    if modality in (\"CT\", \"CTA\"):\n        vol = np.clip(vol, -100, 400)\n        vol = (vol + 100) / 500.0          # 0-1 brain window\n    else:\n        vol = (vol - vol.min()) / (vol.ptp() + 1e-6)\n    return vol.astype(np.float32)\n\n# ------------------------------------------------------------\n# 3 ▸ Dataset class  – handles Polars OR Pandas input\n# ------------------------------------------------------------\nclass RSNADataset(Dataset):\n    def __init__(self, df, cube: int = 112):\n        # ← NEW  ▸ ensure we always hold a Pandas DataFrame internally\n        if isinstance(df, pl.DataFrame):\n            df = df.to_pandas()\n        self.df   = df.reset_index(drop=True)\n        self.cube = cube\n\n    def __len__(self): return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        series_uid = row.SeriesInstanceUID\n\n        vol, mod = load_volume(series_uid)\n        vol      = normalise(vol, mod)\n\n        if vol.ndim == 4:            # (D,H,W,C) ➜ (D,H,W)\n            vol = vol[..., 0]\n\n        # --- centre-crop / pad to cube ----\n        D, H, W = vol.shape\n        tgt     = self.cube\n        scale   = tgt / max(D, H, W)\n\n        # ↓ NEW — compute safe output sizes (≥1 in every dim)\n        if scale < 1.0:\n            new_D = max(1, round(D * scale))\n            new_H = max(1, round(H * scale))\n            new_W = max(1, round(W * scale))\n\n            vol = torch.as_tensor(vol).unsqueeze(0)          # [1,D,H,W]\n            vol = torch.nn.functional.interpolate(\n                vol.unsqueeze(0),                  # [1,1,D,H,W]\n                size=[new_D, new_H, new_W],\n                mode='trilinear',\n                align_corners=False\n            ).squeeze(0).squeeze(0).numpy()        # keep depth dim even if 1\n\n            if vol.ndim == 2:                      # safety: still 2-D? add depth=1\n                vol = vol[np.newaxis, :, :]\n            D, H, W = vol.shape\n\n        pad = [(tgt-D)//2, (tgt-H)//2, (tgt-W)//2]\n        vol = np.pad(vol,\n                     [(pad[0], tgt-D-pad[0]),\n                      (pad[1], tgt-H-pad[1]),\n                      (pad[2], tgt-W-pad[2])],\n                     mode='constant')[:tgt,:tgt,:tgt]\n\n        x = torch.as_tensor(vol).unsqueeze(0)                 # [1,D,H,W]\n        y = torch.tensor(row[LABEL_COLS].values.astype(np.float32))\n        return x, y\n\n# ------------------------------------------------------------\n# 4 ▸ DataLoader & smoke-test\n# ------------------------------------------------------------\nsample_df = train_df.sample(32) if isinstance(train_df, pl.DataFrame) else train_df.sample(32)\nds  = RSNADataset(sample_df)\nldr = DataLoader(ds, batch_size=1, shuffle=False, num_workers=0)   # num_workers=0 → low RAM\n\nxb, yb = next(iter(ldr))\nprint(\"✔️  smoke-test OK — tensor\", xb.shape, \"| labels\", yb.shape)","metadata":{"execution":{"iopub.status.busy":"2025-08-01T06:12:40.849553Z","iopub.execute_input":"2025-08-01T06:12:40.849872Z","iopub.status.idle":"2025-08-01T06:12:41.837874Z","shell.execute_reply.started":"2025-08-01T06:12:40.849842Z","shell.execute_reply":"2025-08-01T06:12:41.837068Z"}}},{"cell_type":"markdown","source":"# Mini-CNN (3-D) + 1-epoch smoke-train","metadata":{}},{"cell_type":"markdown","source":"# =====================================================================\n# STEP 3 ·  mini-CNN (3-D) + 1-epoch smoke-train\n# =====================================================================\nimport torch, torch.nn as nn\nfrom datetime import datetime\nfrom contextlib import nullcontext\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"🚀 device:\", DEVICE)\n\n# ---------- 1 ▸ tiny 3-D CNN -----------------------------------------\nclass Mini3DCNN(nn.Module):\n    \"\"\"Input : [B,1,112,112,112]   Output : [B,14]\"\"\"\n    def __init__(self, n_out: int = 14):\n        super().__init__()\n        self.features = nn.Sequential(\n            nn.Conv3d(1, 8, 3, padding=1), nn.BatchNorm3d(8), nn.ReLU(),\n            nn.MaxPool3d(2),                              # 56³\n            nn.Conv3d(8, 16, 3, padding=1), nn.BatchNorm3d(16), nn.ReLU(),\n            nn.MaxPool3d(2),                              # 28³\n            nn.Conv3d(16, 32, 3, padding=1), nn.BatchNorm3d(32), nn.ReLU(),\n            nn.MaxPool3d(2),                              # 14³\n            nn.Conv3d(32, 64, 3, padding=1), nn.BatchNorm3d(64), nn.ReLU(),\n            nn.AdaptiveAvgPool3d(1),                      # 1×1×1\n        )\n        self.head = nn.Linear(64, n_out)\n\n    def forward(self, x):\n        x = self.features(x)      # [B,64,1,1,1]\n        x = x.flatten(1)          # [B,64]\n        return self.head(x)       # raw logits\n\nmodel = Mini3DCNN().to(DEVICE)\n\n# ---------- 2 ▸ loss, optimiser ---------------------------------------\n# Handle Polars vs. Pandas gracefully\nif isinstance(train_df, pl.DataFrame):\n    pos_frac = train_df.select(LABEL_COLS).to_pandas().mean()   # Series\nelse:\n    pos_frac = train_df[LABEL_COLS].mean()\n\npos_weight = torch.tensor(1.0 / (pos_frac + 1e-3).values,\n                          dtype=torch.float32,\n                          device=DEVICE)\n\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = torch.optim.Adam(model.parameters(), lr=3e-4)\n\nscaler = torch.cuda.amp.GradScaler(enabled=True)\ncm     = torch.cuda.amp.autocast if scaler.is_enabled() else nullcontext\n\n# ---------- 3 ▸ loader (64 random samples) -----------------------------\nif isinstance(train_df, pl.DataFrame):\n    subset_df = train_df.sample(n=64, seed=42)\nelse:                                   # pandas\n    subset_df = train_df.sample(64, random_state=42)\n\ntrain_ld = DataLoader(\n    RSNADataset(subset_df),\n    batch_size=2, shuffle=True, num_workers=0\n)\n\n# ---------- 4 ▸ 1-epoch smoke-train ------------------------------------\nmodel.train()\nrunning = 0.0\nfor step, (xb, yb) in enumerate(train_ld, 1):\n    xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n\n    with cm():\n        logits = model(xb)\n        loss   = criterion(logits, yb)\n\n    scaler.scale(loss).backward()\n    scaler.step(optimizer)\n    scaler.update()\n    optimizer.zero_grad(set_to_none=True)\n\n    running += loss.item()\n    if step == 1 or step % 10 == 0 or step == len(train_ld):\n        print(f\"{datetime.now():%H:%M:%S}  step {step:02d}/{len(train_ld)}  \"\n              f\"loss {running/step:.4f}\")\n\nprint(\"✅ smoke-train finished\")","metadata":{"execution":{"iopub.status.busy":"2025-08-01T06:12:45.060582Z","iopub.execute_input":"2025-08-01T06:12:45.060874Z","iopub.status.idle":"2025-08-01T06:14:24.704962Z","shell.execute_reply.started":"2025-08-01T06:12:45.060854Z","shell.execute_reply":"2025-08-01T06:14:24.704106Z"}}},{"cell_type":"markdown","source":"# Full training loop with validation, early-stopping & checkpointing","metadata":{}},{"cell_type":"markdown","source":"# =====================================================================\n# STEP 4 · multi-epoch train / validation  (early stop + checkpoint)\n# =====================================================================\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom sklearn.metrics import roc_auc_score\nfrom datetime import datetime\n\n# ---------- 1 ▸ train / val split ------------------------------------\nVAL_FRAC   = 0.20           # 20 % validation\nBATCH_SIZE = 4\nEPOCHS     = 2\nPATIENCE   = 3\n\nif isinstance(train_df, pl.DataFrame):\n    val_size = int(len(train_df) * VAL_FRAC)\n    val_df   = train_df.sample(n=val_size, seed=0)         # ← use n=, not frac=\n    train_df_split = train_df.filter(\n        ~pl.col(\"SeriesInstanceUID\").is_in(val_df[\"SeriesInstanceUID\"])\n    )\nelse:                                                       # pandas\n    val_df   = train_df.sample(frac=VAL_FRAC, random_state=0)\n    train_df_split = train_df.drop(val_df.index)\n\ntrain_loader = DataLoader(\n    RSNADataset(train_df_split),\n    batch_size=BATCH_SIZE, shuffle=True, num_workers=0\n)\nval_loader = DataLoader(\n    RSNADataset(val_df),\n    batch_size=BATCH_SIZE, shuffle=False, num_workers=0\n)\n\nprint(f\"train batches: {len(train_loader)} | val batches: {len(val_loader)}\")\n\n# ---------- 2 ▸ fresh model, loss, optimiser, scheduler ---------------\nmodel = Mini3DCNN().to(DEVICE)\n\n# ---------- class-imbalance weights -----------------------------------\nif isinstance(train_df, pl.DataFrame):\n    # convert just the 14 columns to Pandas, then take mean → 1-D array\n    pos_frac = (\n        train_df.select(LABEL_COLS)\n                .to_pandas()\n                .mean()\n                .to_numpy()            # ← 1-D np.array length 14\n    )\nelse:                                   # pandas\n    pos_frac = train_df[LABEL_COLS].mean().to_numpy()\n\npos_weight = torch.tensor(1.0 / (pos_frac + 1e-3),\n                          dtype=torch.float32,\n                          device=DEVICE)\ncriterion  = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n\noptimizer  = torch.optim.Adam(model.parameters(), lr=3e-4, weight_decay=1e-4)\nscheduler  = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)\nscaler     = torch.cuda.amp.GradScaler(enabled=True)\namp_ctx    = torch.cuda.amp.autocast if scaler.is_enabled() else nullcontext\n\n# ---------- 3 ▸ helper: competition-style weighted AUC ----------------\nCLASS_WEIGHTS = torch.tensor([13 if c == 'Aneurysm Present' else 1\n                              for c in LABEL_COLS], dtype=torch.float32)\n\ndef weighted_auc(y_true, y_pred):\n    aucs = []\n    for c in range(y_true.shape[1]):\n        if (y_true[:, c] == 0).all() or (y_true[:, c] == 1).all():\n            aucs.append(0.5)                       # neutral\n        else:\n            aucs.append(roc_auc_score(y_true[:, c], y_pred[:, c]))\n    w = CLASS_WEIGHTS / CLASS_WEIGHTS.sum()\n    return float((torch.tensor(aucs) * w).sum())\n\n# ---------- 4 ▸ train & validate epoch --------------------------------\ndef run_epoch(loader, train_mode=True):\n    model.train() if train_mode else model.eval()\n    running, y_true, y_pred = 0.0, [], []\n\n    for xb, yb in loader:\n        xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n\n        with amp_ctx():\n            logits = model(xb)\n            loss   = criterion(logits, yb)\n\n        if train_mode:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad(set_to_none=True)\n\n        running += loss.item() * xb.size(0)\n        y_true.append(yb.cpu())\n        y_pred.append(torch.sigmoid(logits).detach().cpu())\n\n    y_true = torch.cat(y_true).numpy()\n    y_pred = torch.cat(y_pred).numpy()\n    loss   = running / len(loader.dataset)\n    auc    = weighted_auc(y_true, y_pred)\n    return loss, auc\n\n# ---------- 5 ▸ main training loop ------------------------------------\nbest_auc, patience_left = 0.0, PATIENCE\nfor epoch in range(1, EPOCHS + 1):\n    trn_loss, trn_auc = run_epoch(train_loader, train_mode=True)\n    val_loss, val_auc = run_epoch(val_loader,  train_mode=False)\n    scheduler.step()\n\n    print(f\"{datetime.now():%H:%M:%S}  \"\n          f\"Ep {epoch:02d} | \"\n          f\"train {trn_loss:.4f}/{trn_auc:.3f}  |  \"\n          f\"val {val_loss:.4f}/{val_auc:.3f}\")\n\n    if val_auc > best_auc:\n        best_auc, patience_left = val_auc, PATIENCE\n        torch.save(model.state_dict(), \"best_model.pt\")\n        print(\"  ✅ saved best_model.pt\")\n    else:\n        patience_left -= 1\n        if patience_left == 0:\n            print(\"  ⏹ early stop\")\n            break\n\nprint(f\"🏁 best val-AUC: {best_auc:.3f}\")","metadata":{"execution":{"iopub.status.busy":"2025-08-01T06:14:24.706442Z","iopub.execute_input":"2025-08-01T06:14:24.707066Z"}}},{"cell_type":"markdown","source":"# Inference & submission  (RSNAInferenceServer)","metadata":{}},{"cell_type":"markdown","source":"# =====================================================================\n# STEP 5 · inference & submission  (RSNAInferenceServer)\n# =====================================================================\nimport torch, polars as pl, shutil, math, numpy as np, pydicom, os\nfrom pathlib import Path\nimport kaggle_evaluation.rsna_inference_server as rsna_srv\n\nID_COL = 'SeriesInstanceUID'\nLABEL_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present',\n]\n\n# ---------- 1 ▸ load checkpoint ---------------------------------------\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nmodel  = Mini3DCNN().to(DEVICE)\nmodel.load_state_dict(torch.load(\"best_model.pt\", map_location=DEVICE))\nmodel.eval()\n\n# ---------- 2 ▸ tiny helper to turn a series folder into a 112³ tensor -\n@torch.no_grad()\ndef series_to_tensor(series_path: str):\n    uid  = os.path.basename(series_path.rstrip(\"/\"))\n    vol, mod = load_volume(uid)               # uses same cached loader\n    if vol.ndim == 4:                         # squeeze channels if any\n        vol = vol[..., 0]\n    vol = normalise(vol, mod)\n\n    # ensure ≤112³ tensor just like training\n    D, H, W = vol.shape\n    tgt, s  = 112, 112 / max(D, H, W)\n    if s < 1.0:\n        new_D = max(1, round(D * s))\n        new_H = max(1, round(H * s))\n        new_W = max(1, round(W * s))\n        vol = torch.as_tensor(vol).unsqueeze(0)\n        vol = torch.nn.functional.interpolate(\n            vol.unsqueeze(0),\n            size=[new_D, new_H, new_W],\n            mode='trilinear', align_corners=False\n        ).squeeze(0).squeeze(0).numpy()\n        if vol.ndim == 2:                     # keep depth dim\n            vol = vol[np.newaxis, :, :]\n    D, H, W = vol.shape\n    pad = [(tgt-D)//2, (tgt-H)//2, (tgt-W)//2]\n    vol = np.pad(\n        vol,\n        [(pad[0], tgt-D-pad[0]),\n         (pad[1], tgt-H-pad[1]),\n         (pad[2], tgt-W-pad[2])],\n        mode='constant'\n    )[:tgt,:tgt,:tgt]\n\n    tensor = torch.as_tensor(vol, dtype=torch.float32, device=DEVICE).unsqueeze(0).unsqueeze(0)\n    return tensor\n\n# ---------- 3 ▸ predict() for the evaluation API ----------------------\ndef predict(series_path: str) -> pl.DataFrame:\n    try:\n        uid   = os.path.basename(series_path.rstrip(\"/\"))\n        xb    = series_to_tensor(series_path)\n        with torch.cuda.amp.autocast():\n            logits = model(xb)\n            probs  = torch.sigmoid(logits).cpu().numpy().ravel()\n\n        preds_df = pl.DataFrame(\n            data=[[uid] + probs.tolist()],\n            schema=[ID_COL, *LABEL_COLS],\n            orient='row'\n        ).drop(ID_COL)\n\n    except Exception as e:\n        print(f\"[WARN] predict() failed for {uid}: {e}\")\n        preds_df = pl.DataFrame(\n            data=[[uid] + [0.1]*len(LABEL_COLS)],\n            schema=[ID_COL, *LABEL_COLS],\n            orient='row'\n        ).drop(ID_COL)\n\n    # clean shared tmp to avoid disk-full errors\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n    return preds_df\n\n# ---------- 4 ▸ hook into Kaggle's gateway ----------------------------\nserver = rsna_srv.RSNAInferenceServer(predict)\n\nif os.getenv(\"KAGGLE_IS_COMPETITION_RERUN\"):\n    server.serve()               # >>> RUNS ON ACTUAL TEST SET <<< //\nelse:\n    server.run_local_gateway()   # local demo → writes submission.parquet\n    display(pl.read_parquet('/kaggle/working/submission.parquet'))","metadata":{}},{"cell_type":"code","source":"# ==============================================================\n# RSNA • “constant 0.5” baseline\n# ==============================================================\n\nimport os, shutil, pandas as pd\nimport kaggle_evaluation.rsna_inference_server as rsna_srv\n\nID_COL = \"SeriesInstanceUID\"\nLABEL_COLS = [\n    'Left Infraclinoid Internal Carotid Artery',\n    'Right Infraclinoid Internal Carotid Artery',\n    'Left Supraclinoid Internal Carotid Artery',\n    'Right Supraclinoid Internal Carotid Artery',\n    'Left Middle Cerebral Artery',\n    'Right Middle Cerebral Artery',\n    'Anterior Communicating Artery',\n    'Left Anterior Cerebral Artery',\n    'Right Anterior Cerebral Artery',\n    'Left Posterior Communicating Artery',\n    'Right Posterior Communicating Artery',\n    'Basilar Tip',\n    'Other Posterior Circulation',\n    'Aneurysm Present',\n]\n\n# --------------------------------------------------------------\n# predict(): returns a single-row DataFrame of 0.5 probabilities\n# --------------------------------------------------------------\ndef predict(series_path: str) -> pd.DataFrame:\n    uid = os.path.basename(series_path.rstrip(\"/\"))\n    \n    preds = pd.DataFrame(\n        [[uid] + [0.5]*len(LABEL_COLS)],\n        columns=[ID_COL] + LABEL_COLS\n    )\n    \n    # evaluation API only wants label columns\n    preds = preds.drop(columns=[ID_COL])\n    \n    # scrub temp area to keep disk usage tiny\n    shutil.rmtree(\"/kaggle/shared\", ignore_errors=True)\n    \n    return preds\n\n# --------------------------------------------------------------\n# plug into Kaggle’s inference gateway\n# --------------------------------------------------------------\nserver = rsna_srv.RSNAInferenceServer(predict)\n\nif os.getenv(\"KAGGLE_IS_COMPETITION_RERUN\"):   # ← real test set\n    server.serve()\nelse:                                          # ← local smoke-test\n    server.run_local_gateway()                 # writes submission.parquet\n    display(pd.read_parquet(\"/kaggle/working/submission.parquet\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-02T10:31:46.209133Z","iopub.execute_input":"2025-08-02T10:31:46.209386Z","iopub.status.idle":"2025-08-02T10:31:54.024556Z","shell.execute_reply.started":"2025-08-02T10:31:46.209359Z","shell.execute_reply":"2025-08-02T10:31:54.024002Z"}},"outputs":[],"execution_count":null}]}