{"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.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":99552,"databundleVersionId":13190393,"sourceType":"competition"},{"sourceId":519793,"sourceType":"modelInstanceVersion","modelInstanceId":409304,"modelId":427151}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CTA Inference for RSNA 2025 Intracranial Aneurysm Detection\n\nThis notebook will infer the CTA series from the trained model.\n\n📦 **Dataset available here:**  \n🔗 [RSNA 2025 IA CTA 224 Tensors – on Kaggle](https://www.kaggle.com/datasets/dennisfong/rsna-2025-ia-cta-224-tensors)\n\n---\n\nWhole Process from precache, training to inference:\n\n📦 **Precaching Notebook available here:**  \n🔗 [2.5D EfficientNet - RSNA CTA - Precache Tensor](https://www.kaggle.com/code/dennisfong/2-5d-efficientnet-rsna-cta-precache-tensor)\n\n📦 **Training Notebook available here:**  \n🔗 [2.5D EfficientNet - RSNA CTA - Training (You are reading)](https://www.kaggle.com/code/dennisfong/2-5d-efficientnet-rsna-cta-training)\n\n📦 **Inference Notebook available here:**  \n🔗 [2.5D EfficientNet - RSNA CTA - Inference](https://www.kaggle.com/code/dennisfong/2-5d-efficientnet-rsna-cta-inference)\n\n---\n\n# 🚀 Help Others Discover This Work!\n\n## 👍 If the notebooks or dataset were helpful, **please give it an upvote**!\n\nYour support is appreciated and keeps the community growing. 🙌\n\n## 🌟 Please **UPVOTE** if you found it helpful!\n\n---\n","metadata":{}},{"cell_type":"code","source":"\nimport os\nimport gc\nimport re\nimport cv2\nimport math\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport pydicom\nimport torch\nimport torch.nn as nn\nimport timm\nfrom collections import defaultdict\nfrom typing import List, Tuple\nimport shutil\nfrom sklearn.metrics import roc_auc_score\n\n# Kaggle server\nimport kaggle_evaluation.rsna_inference_server\n\n# ========= Competition schema =========\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# Optional allowlist (not used in modeling; provided for compliance/reference)\nDICOM_TAG_ALLOWLIST = [\n    'BitsAllocated','BitsStored','Columns','FrameOfReferenceUID','HighBit',\n    'ImageOrientationPatient','ImagePositionPatient','InstanceNumber','Modality',\n    'PatientID','PhotometricInterpretation','PixelRepresentation','PixelSpacing',\n    'PlanarConfiguration','RescaleIntercept','RescaleSlope','RescaleType','Rows',\n    'SOPClassUID','SOPInstanceUID','SamplesPerPixel','SliceThickness',\n    'SpacingBetweenSlices','StudyInstanceUID','TransferSyntaxUID',\n]\n\n# ========= Inference config =========\nIMG_SIZE = 224\nOFFSETS = (-2, -1, 0, 1, 2)   # window length 5\nIN_CHANS = len(OFFSETS)\nBATCH_SIZE = 16\nAGGREGATE = \"max\"  # max/mean/topk_mean\nUSE_ROI = False     # coords not available on test → use same stream for full+roi\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# Where model weights are stored. Attach a Dataset with your .pth files if needed.\nCANDIDATE_MODEL_DIRS = [\n    \"/kaggle/input/rsna-2025-ia-ct-224-efficientnet/pytorch/default/1\",        # attached datasets\n    \"/kaggle/working\",      # runtime dir\n    \".\",                    # current dir\n]\n\n# ========= Model definition (Hybrid full + ROI + coords) =========\nclass HybridAneurysmModel(nn.Module):\n    def __init__(self, base_model_name: str, num_classes: int):\n        super().__init__()\n        self.backbone = timm.create_model(base_model_name, in_chans=IN_CHANS, num_classes=0, pretrained=False)\n        self.feature_dim = self.backbone.num_features\n        self.coord_fc = nn.Sequential(nn.Linear(2, 32), nn.ReLU(), nn.Linear(32, 64))\n        self.fc = nn.Sequential(nn.Dropout(0.3), nn.Linear(self.feature_dim * 2 + 64, num_classes))\n\n    def forward(self, x_full: torch.Tensor, x_roi: torch.Tensor, coords: torch.Tensor) -> torch.Tensor:\n        f_full = self.backbone(x_full)\n        f_roi  = self.backbone(x_roi)\n        f_coord = self.coord_fc(coords.float())\n        return self.fc(torch.cat([f_full, f_roi, f_coord], dim=1))\n\n# ========= Helpers =========\ndef sort_dicom_slices(filepaths: List[str]):\n    dicoms = [pydicom.dcmread(fp, force=True) for fp in filepaths]\n    try:\n        dicoms.sort(key=lambda d: float(d.ImagePositionPatient[2]))\n    except Exception:\n        dicoms.sort(key=lambda d: int(getattr(d, 'InstanceNumber', 0)))\n    return dicoms\n\ndef series_to_tensor_chw(dicoms) -> np.ndarray:\n    # Resize all to IMG_SIZE and per-series z-score normalization\n    resized = []\n    for d in dicoms:\n        arr = d.pixel_array\n        if arr is None or arr.size == 0:\n            continue\n        arr = arr.astype(np.float32)\n        arr = cv2.resize(arr, (IMG_SIZE, IMG_SIZE), interpolation=cv2.INTER_AREA)\n        resized.append(arr)\n    if len(resized) == 0:\n        # fallback to zeros to avoid crashes (rare)\n        vol = np.zeros((1, IMG_SIZE, IMG_SIZE), dtype=np.float32)\n    else:\n        vol = np.stack(resized, axis=0)  # [N,H,W]\n    mean = float(vol.mean())\n    std = float(vol.std()) + 1e-6\n    vol = (vol - mean) / std\n    # return as CHW for convenience when building windows\n    return np.transpose(vol, (0, 1, 2))  # still [N,H,W]; windows will transpose to CHW later\n\ndef take_window_from_volume(vol_nhw: np.ndarray, center_idx: int, offsets=OFFSETS) -> np.ndarray:\n    # vol_nhw: [N,H,W] float32\n    N = vol_nhw.shape[0]\n    idxs = [min(max(0, center_idx + o), N - 1) for o in offsets]\n    win = vol_nhw[idxs, :, :]              # [len(offsets),H,W]\n    return win.astype(np.float32, copy=False)\n\ndef coords_to_px(coords: np.ndarray, img_size: int) -> Tuple[int, int]:\n    # coords are zeros on test; keep util for API compatibility\n    x, y = float(coords[0]), float(coords[1])\n    if 0.0 <= x <= 1.0 and 0.0 <= y <= 1.0:\n        x *= img_size; y *= img_size\n    return int(round(x)), int(round(y))\n\ndef crop_and_resize_chw(img_chw: np.ndarray, x1: int, y1: int, x2: int, y2: int, out_size: int) -> np.ndarray:\n    img_hwc = np.transpose(np.asarray(img_chw), (1, 2, 0))\n    crop = img_hwc[y1:y2, x1:x2]\n    if crop.size == 0 or crop.shape[0] < 2 or crop.shape[1] < 2:\n        crop = img_hwc\n    crop = crop.astype(np.float32, copy=False)\n    crop = np.ascontiguousarray(crop)\n    crop = cv2.resize(crop, (out_size, out_size), interpolation=cv2.INTER_AREA)\n    return np.transpose(crop, (2, 0, 1))\n\ndef window_to_full_and_roi(win_chw: np.ndarray, coords: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:\n    if USE_ROI and np.any(coords != 0):\n        cx, cy = coords_to_px(coords, IMG_SIZE)\n        r = max(12, int(0.15 * IMG_SIZE))\n        x1 = max(0, cx - r); y1 = max(0, cy - r)\n        x2 = min(IMG_SIZE - 1, cx + r); y2 = min(IMG_SIZE - 1, cy + r)\n        roi = crop_and_resize_chw(win_chw, x1, y1, x2, y2, IMG_SIZE)\n        return win_chw, roi\n    # No coords on test → identical streams\n    return win_chw, win_chw\n\n# ========= Checkpoint discovery/loading =========\n_ckpt_cache = None  # type: ignore\n_models = None      # type: ignore\n\n_ckpt_regex = re.compile(r\"([^/\\\\]+)_hybrid_fold(\\d+)\\.pth$\")\n\ndef discover_checkpoints() -> List[Tuple[str, str]]:\n    # Returns list of (arch_name, path)\n    found: List[Tuple[str, str]] = []\n    for base in CANDIDATE_MODEL_DIRS:\n        if not os.path.isdir(base):\n            continue\n        for root, _, files in os.walk(base):\n            for f in files:\n                if f.endswith('.pth') and ('_hybrid_fold' in f or '_best_wAUC' in f):\n                    m = _ckpt_regex.search(f)\n                    if m:\n                        arch = m.group(1)\n                    else:\n                        # heuristic: arch is everything before first _fold or _hybrid\n                        arch = f.split('_hybrid_fold')[0].split('_fold')[0]\n                    found.append((arch, os.path.join(root, f)))\n    # stable ordering\n    found.sort(key=lambda x: x[1])\n    return found\n\ndef load_hybrid_model(arch_name: str, weight_path: str) -> nn.Module:\n    model = HybridAneurysmModel(base_model_name=arch_name, num_classes=len(LABEL_COLS))\n    state = torch.load(weight_path, map_location=DEVICE)\n    if isinstance(state, dict) and any(k.startswith('module.') for k in state.keys()):\n        state = {k.replace('module.', '', 1): v for k, v in state.items()}\n    model.load_state_dict(state, strict=True)\n    model.eval().to(DEVICE)\n    return model\n\ndef get_models() -> List[Tuple[str, nn.Module]]:\n    global _ckpt_cache, _models\n    if _models is not None:\n        return _models\n    _ckpt_cache = discover_checkpoints()\n    if not _ckpt_cache:\n        raise FileNotFoundError('No model checkpoints found. Attach a dataset with *_hybrid_fold*.pth files.')\n    mods: List[Tuple[str, nn.Module]] = []\n    for arch, path in _ckpt_cache:\n        try:\n            m = load_hybrid_model(arch, path)\n            mods.append((arch, m))\n        except Exception:\n            # skip incompatible files\n            continue\n    if not mods:\n        raise RuntimeError('Failed to load any checkpoints from discovered files.')\n    _models = mods\n    print(f\"Loaded {len(_models)} models\")\n    return _models\n\n# ========= Per-series sliding-window inference =========\n@torch.no_grad()\ndef predict_series_probs(dicoms) -> np.ndarray:\n    models = get_models()\n    # Build normalized volume [N,H,W]\n    vol = series_to_tensor_chw(dicoms)\n    N = vol.shape[0]\n    # Prepare coords zeros on test\n    coords = np.zeros((N, 2), dtype=np.float32)\n\n    all_model_probs = []\n    for _, model in models:\n        batch_full, batch_roi, batch_coords = [], [], []\n        probs_accum = []\n        for c in range(N):\n            win = take_window_from_volume(vol, c, OFFSETS)   # [C,H,W]\n            win_chw = np.transpose(win, (0, 1, 2))           # still [C,H,W]\n            full_chw, roi_chw = window_to_full_and_roi(win_chw, coords[c])\n            batch_full.append(full_chw)\n            batch_roi.append(roi_chw)\n            batch_coords.append(coords[c])\n            # flush by batch\n            if len(batch_full) == BATCH_SIZE or c == N - 1:\n                xb_full = torch.from_numpy(np.stack(batch_full).astype(np.float32)).to(DEVICE)\n                xb_roi  = torch.from_numpy(np.stack(batch_roi).astype(np.float32)).to(DEVICE)\n                cb      = torch.from_numpy(np.stack(batch_coords).astype(np.float32)).to(DEVICE)\n                logits = model(xb_full, xb_roi, cb)\n                probs = torch.sigmoid(logits).cpu().numpy()\n                probs_accum.append(probs)\n                batch_full.clear(); batch_roi.clear(); batch_coords.clear()\n        probs_all = np.concatenate(probs_accum, axis=0) if probs_accum else np.zeros((1, len(LABEL_COLS)), dtype=np.float32)\n        if AGGREGATE == 'max':\n            series_prob = probs_all.max(axis=0)\n        elif AGGREGATE == 'mean':\n            series_prob = probs_all.mean(axis=0)\n        else:  # topk_mean\n            k = max(1, N // 5)\n            series_prob = np.sort(probs_all, axis=0)[-k:].mean(axis=0)\n        all_model_probs.append(series_prob)\n        # free memory between models\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n    # ensemble (probability average)\n    return np.mean(np.stack(all_model_probs, axis=0), axis=0)\n\n# ========= Kaggle-required predict(series_path) =========\ndef predict(series_path: str) -> pl.DataFrame | pd.DataFrame:\n    series_id = os.path.basename(series_path)\n\n    # Try reading just one DICOM to check the Modality\n    first_dcm = None\n    for root, _, files in os.walk(series_path):\n        for f in files:\n            if f.endswith('.dcm'):\n                try:\n                    first_dcm = pydicom.dcmread(os.path.join(root, f), stop_before_pixels=True)\n                    break\n                except Exception:\n                    continue\n        if first_dcm:\n            break\n\n    # Check modality\n    modality = getattr(first_dcm, 'Modality', '').upper() if first_dcm else ''\n    if modality != 'CT':\n        zeros = [[series_id] + [0.0] * len(LABEL_COLS)]\n        predictions = pl.DataFrame(data=zeros, schema=[ID_COL, *LABEL_COLS], orient='row')\n        return predictions.drop(ID_COL)\n\n    # Proceed with full DICOM loading and processing\n    filepaths = []\n    for root, _, files in os.walk(series_path):\n        for f in files:\n            if f.endswith('.dcm'):\n                filepaths.append(os.path.join(root, f))\n    dicoms = sort_dicom_slices(filepaths)\n\n    # Inference\n    probs = predict_series_probs(dicoms)\n\n    # Build output (one row)\n    data = [[series_id] + probs.tolist()]\n    predictions = pl.DataFrame(data=data, schema=[ID_COL, *LABEL_COLS], orient='row')\n\n    # Required cleanup to avoid disk pressure\n    shutil.rmtree('/kaggle/shared', ignore_errors=True)\n\n    # Server expects features only (without ID_COL)\n    return predictions.drop(ID_COL)\n\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2025-08-14T02:13:48.566315Z","iopub.execute_input":"2025-08-14T02:13:48.566586Z","iopub.status.idle":"2025-08-14T02:14:03.680495Z","shell.execute_reply.started":"2025-08-14T02:13:48.566564Z","shell.execute_reply":"2025-08-14T02:14:03.679898Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SERIES_PATH = \"/kaggle/input/rsna-intracranial-aneurysm-detection/series/\"\nDESIRED_SERIES = \"1.2.826.0.1.3680043.8.498.10004044428023505108375152878107656647\"\n#\nimport pandas as pd\nimport random\nfrom IPython.display import display\n\n# Load the data\ndf = pd.read_csv(\"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\")\n\n# Filter only CTA modality\ncta_df = df[df['Modality'] == 'CTA']\n\n# Pick a random SeriesInstanceUID from CTA cases\nDESIRED_SERIES = random.choice(cta_df['SeriesInstanceUID'].unique())\n\n# Filter the CTA DataFrame for the selected SeriesInstanceUID\nfiltered_df = cta_df[cta_df['SeriesInstanceUID'] == DESIRED_SERIES].reset_index(drop=True)\n\n# Display\nprint(\"Randomly selected SeriesInstanceUID:\", DESIRED_SERIES)\ndisplay(filtered_df)\n","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:03.681837Z","iopub.execute_input":"2025-08-14T02:14:03.682236Z","iopub.status.idle":"2025-08-14T02:14:03.740706Z","shell.execute_reply.started":"2025-08-14T02:14:03.682216Z","shell.execute_reply":"2025-08-14T02:14:03.740147Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    p = predict(SERIES_PATH+DESIRED_SERIES)\n    display(p)","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:03.741471Z","iopub.execute_input":"2025-08-14T02:14:03.741711Z","iopub.status.idle":"2025-08-14T02:14:31.245453Z","shell.execute_reply.started":"2025-08-14T02:14:03.741692Z","shell.execute_reply":"2025-08-14T02:14:31.244682Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_weighted_auc(y_true, y_prob, class_names):\n    \"\"\"Robust wAUC: handles single-column vs multi-class shape mismatches.\n    - If one of y_true/y_prob has 1 column and class_names has 14, align to AP only.\n    - Otherwise, truncate to the minimum common number of classes.\n    - Skips classes with single-label targets or constant predictions.\n    Returns (weighted_auc, ap_auc, others_mean, per_class_auc_dict, skipped_names)\n    \"\"\"\n    y_true = np.atleast_2d(np.asarray(y_true))\n    y_prob = np.atleast_2d(np.asarray(y_prob))\n\n    # Ensure 2D (N, C)\n    if y_true.ndim != 2:\n        y_true = y_true.reshape(y_true.shape[0], -1)\n    if y_prob.ndim != 2:\n        y_prob = y_prob.reshape(y_prob.shape[0], -1)\n\n    C_true = y_true.shape[1]\n    C_prob = y_prob.shape[1]\n    C_names = len(class_names)\n\n    ap_name = \"Aneurysm Present\"\n    # Case: one side is binary (C==1) and the other has many classes → evaluate AP only\n    if (C_true == 1 and C_prob > 1) or (C_prob == 1 and C_true > 1) or (C_true == 1 and C_prob == 1 and C_names != 1):\n        if ap_name in class_names and max(C_true, C_prob) >= 1:\n            ap_idx = class_names.index(ap_name) if C_names > 1 else 0\n            if C_true > 1:\n                y_true = y_true[:, [ap_idx]] if ap_idx < C_true else y_true[:, [C_true - 1]]\n            if C_prob > 1:\n                y_prob = y_prob[:, [ap_idx]] if ap_idx < C_prob else y_prob[:, [C_prob - 1]]\n            class_names = [ap_name]\n        else:\n            # Fallback: just keep the first column from both\n            y_true = y_true[:, [0]]\n            y_prob = y_prob[:, [0]]\n            class_names = [ap_name] if C_names >= 1 else [\"class0\"]\n    else:\n        # General case: align by truncating to minimum number of classes\n        K = min(C_true, C_prob, C_names)\n        if K <= 0:\n            raise ValueError(f\"No common classes to evaluate: y_true {y_true.shape}, y_prob {y_prob.shape}, names {C_names}\")\n        if (C_true != K) or (C_prob != K) or (C_names != K):\n            y_true = y_true[:, :K]\n            y_prob = y_prob[:, :K]\n            class_names = list(class_names)[:K]\n\n    # Now shapes must match\n    assert y_true.shape == y_prob.shape, f\"shape mismatch after alignment: {y_true.shape} vs {y_prob.shape}\"\n\n    aucs, skipped = {}, []\n    for i, name in enumerate(class_names):\n        yi = y_true[:, i]\n        pi = y_prob[:, i]\n        # skip if only one class present, or predictions are constant\n        if np.unique(yi).size < 2 or np.allclose(pi, pi[0]):\n            skipped.append(name)\n            continue\n        try:\n            aucs[name] = roc_auc_score(yi, pi)\n        except ValueError:\n            skipped.append(name)\n\n    ap_auc = aucs.get(ap_name, np.nan)\n    others = [v for k, v in aucs.items() if k != ap_name]\n    others_mean = np.mean(others) if len(others) else np.nan\n\n    # if either term is missing, use the one available\n    if np.isnan(ap_auc) and np.isnan(others_mean):\n        weighted_auc = np.nan\n    elif np.isnan(ap_auc):\n        weighted_auc = others_mean\n    elif np.isnan(others_mean):\n        weighted_auc = ap_auc\n    else:\n        weighted_auc = 0.5 * (ap_auc + others_mean)\n\n    return weighted_auc, ap_auc, others_mean, aucs, skipped","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:31.246191Z","iopub.execute_input":"2025-08-14T02:14:31.246394Z","iopub.status.idle":"2025-08-14T02:14:31.257088Z","shell.execute_reply.started":"2025-08-14T02:14:31.246376Z","shell.execute_reply":"2025-08-14T02:14:31.256383Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\"\"\"\nall_probs, all_targets = [], []\n#y   = pd.to_numeric(filtered_df[LABEL_COLS], errors=\"coerce\").fillna(0.0).values.astype(np.float32)\ny = filtered_df[LABEL_COLS].apply(pd.to_numeric, errors=\"coerce\").fillna(0.0).values.astype(np.float32)\n\nall_probs.append(p)\nall_targets.append(y)\n\nprint(\"y shape:\", y.shape)\nprint(\"p shape:\", p.shape)\nprint(\"Labels:\", LABEL_COLS)\nprint(\"Unique values per class:\")\nfor i, name in enumerate(LABEL_COLS):\n    print(f\"{name}: {np.unique(y[:, i])}\")\n\n\nwAUC, ap_auc, others_mean, per_class_auc, skipped = compute_weighted_auc(all_targets, all_probs, LABEL_COLS)\nprint(f\"wAUC {wAUC:.4f} | AP {ap_auc:.4f} | others {others_mean:.4f}\")\n\"\"\"","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:31.258544Z","iopub.execute_input":"2025-08-14T02:14:31.258737Z","iopub.status.idle":"2025-08-14T02:14:31.296867Z","shell.execute_reply.started":"2025-08-14T02:14:31.258721Z","shell.execute_reply":"2025-08-14T02:14:31.29627Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\n\"\"\"\n# ==== Multi-series evaluation (balanced sample) to avoid NaN AUCs ====\nimport os, random, numpy as np, pandas as pd\n\ndef df_to_numpy(df):\n    try:\n        import polars as pl\n        if isinstance(df, pl.DataFrame):\n            return df.to_numpy()\n    except Exception:\n        pass\n    if isinstance(df, pd.DataFrame):\n        return df.values\n    return np.asarray(df)\n\n# Load train metadata and filter CTA\ntrain_df = pd.read_csv(\"/kaggle/input/rsna-intracranial-aneurysm-detection/train.csv\")\ncta_df = train_df[train_df['Modality'] == 'CTA'].copy()\n\n# Balanced sample of positives/negatives for \"Aneurysm Present\"\npos_ids = cta_df[cta_df['Aneurysm Present'] == 1][ID_COL].astype(str).tolist()\nneg_ids = cta_df[cta_df['Aneurysm Present'] == 0][ID_COL].astype(str).tolist()\n\nk_per_class = 50  # adjust if you want more/less\nsample_ids = random.sample(pos_ids, min(k_per_class, len(pos_ids))) + \\\n             random.sample(neg_ids, min(k_per_class, len(neg_ids)))\nrandom.shuffle(sample_ids)\n\nall_probs, all_targets = [], []\nfailed = []\nfor sid in sample_ids:\n    series_path = os.path.join(SERIES_PATH, sid)\n    try:\n        p_df = predict(series_path)  # returns 1x14 DataFrame without ID\n        p = df_to_numpy(p_df).astype(np.float32)\n        y = (cta_df.loc[cta_df[ID_COL].astype(str) == sid, LABEL_COLS]\n                    .apply(pd.to_numeric, errors='coerce')\n                    .fillna(0.0)\n                    .values\n                    .astype(np.float32))\n        # flatten (1,14) -> (14,)\n        if p.ndim == 2 and p.shape[0] == 1:\n            p = p[0]\n        if y.ndim == 2 and y.shape[0] == 1:\n            y = y[0]\n        all_probs.append(p)\n        all_targets.append(y)\n    except Exception as e:\n        failed.append((sid, str(e)))\n\nall_probs = np.asarray(all_probs, dtype=np.float32)\nall_targets = np.asarray(all_targets, dtype=np.float32)\n\nprint(\"Eval N:\", all_targets.shape, \"Failed:\", len(failed))\nwAUC, ap_auc, others_mean, per_class_auc, skipped = compute_weighted_auc(all_targets, all_probs, LABEL_COLS)\nprint(f\"wAUC {wAUC:.4f} | AP {ap_auc:.4f} | others {others_mean:.4f} | skipped: {skipped[:5]}{'...' if len(skipped)>5 else ''}\")\n\"\"\"","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:31.297592Z","iopub.execute_input":"2025-08-14T02:14:31.297795Z","iopub.status.idle":"2025-08-14T02:14:31.311505Z","shell.execute_reply.started":"2025-08-14T02:14:31.297752Z","shell.execute_reply":"2025-08-14T02:14:31.31073Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ========= Start RSNA server =========\ninference_server = kaggle_evaluation.rsna_inference_server.RSNAInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway()\n    display(pl.read_parquet('/kaggle/working/submission.parquet'))","metadata":{"execution":{"iopub.status.busy":"2025-08-14T02:14:31.3123Z","iopub.execute_input":"2025-08-14T02:14:31.312564Z","iopub.status.idle":"2025-08-14T02:15:25.323385Z","shell.execute_reply.started":"2025-08-14T02:14:31.31254Z","shell.execute_reply":"2025-08-14T02:15:25.322726Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}