{"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.12.13"},"papermill":{"default_parameters":{},"duration":474.316085,"end_time":"2026-08-05T17:51:47.676276+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-08-05T17:43:53.360191+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"bf5a6c1e","cell_type":"code","source":"import os, random, glob\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport pydicom\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nSEED = 42\nN_SLICES = 9\nIMAGE_SIZE = 224\nBATCH_SIZE = 8\nEPOCHS = 2                 # Increase after the end-to-end check succeeds\nNUM_WORKERS = 2\nDEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n\ndef seed_everything(seed=SEED):\n    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\nseed_everything()\n\ntrain_csvs = glob.glob('/kaggle/input/**/train.csv', recursive=True)\nassert train_csvs, 'Attach the competition dataset in the Notebook Input panel.'\nDATA_DIR = Path(train_csvs[0]).parent\nprint(f'DATA_DIR: {DATA_DIR}')\nprint('Device:', DEVICE)","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:43:56.007061Z","iopub.status.busy":"2026-08-05T17:43:56.006386Z","iopub.status.idle":"2026-08-05T17:51:28.137378Z","shell.execute_reply":"2026-08-05T17:51:28.136531Z"},"papermill":{"duration":452.137513,"end_time":"2026-08-05T17:51:28.140937+00:00","exception":false,"start_time":"2026-08-05T17:43:56.003424+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"f489024a","cell_type":"code","source":"train = pd.read_csv(DATA_DIR / 'train.csv')\ntrain_series = pd.read_csv(DATA_DIR / 'train_series.csv')\ntest = pd.read_csv(DATA_DIR / 'test.csv')\ntest_series = pd.read_csv(DATA_DIR / 'test_series.csv')\nsample_sub = pd.read_csv(DATA_DIR / 'sample_submission.csv')\n\nID_COL = 'StudyInstanceUID'\nLABELS = [c for c in sample_sub.columns if c != ID_COL]\nassert set(LABELS).issubset(train.columns)\nprint('train / test:', train.shape, test.shape)\nprint('labels:', LABELS)\nprint('fully labelled studies:', train[LABELS].notna().all(axis=1).sum())\ndisplay(train.head(2))\ndisplay(train[LABELS].mean().sort_values().to_frame('prevalence'))\ndisplay(train_series[['Anatomical_Plane', 'Fluid_Sensitive', 'Fat_Suppression']].value_counts().head(12))","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:28.145999Z","iopub.status.busy":"2026-08-05T17:51:28.145071Z","iopub.status.idle":"2026-08-05T17:51:28.442979Z","shell.execute_reply":"2026-08-05T17:51:28.44233Z"},"papermill":{"duration":0.30178,"end_time":"2026-08-05T17:51:28.444402+00:00","exception":false,"start_time":"2026-08-05T17:51:28.142622+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"91b1869b","cell_type":"code","source":"def preferred_series(series_df):\n    s = series_df.copy()\n    s['_rank'] = (s['Anatomical_Plane'].eq('Sagittal').astype(int) * 4\n                  + s['Fluid_Sensitive'].fillna(0).astype(int) * 2\n                  + s['Fat_Suppression'].fillna(0).astype(int))\n    return s.sort_values([ID_COL, '_rank', 'SeriesInstanceUID'], ascending=[True, False, True]).drop_duplicates(ID_COL)\n\ntrain_pick = preferred_series(train_series)[[ID_COL, 'SeriesInstanceUID']]\ntest_pick = preferred_series(test_series)[[ID_COL, 'SeriesInstanceUID']]\ntrain_labeled = train.loc[train[LABELS].notna().all(axis=1)].merge(train_pick, on=ID_COL, how='inner').reset_index(drop=True)\ntest_data = test[[ID_COL]].merge(test_pick, on=ID_COL, how='left').reset_index(drop=True)\nassert len(test_data) == len(test), 'Some test studies have no selected series.'\nprint('trainable studies:', len(train_labeled), '| test studies:', len(test_data))","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:28.450451Z","iopub.status.busy":"2026-08-05T17:51:28.449736Z","iopub.status.idle":"2026-08-05T17:51:28.514406Z","shell.execute_reply":"2026-08-05T17:51:28.513541Z"},"papermill":{"duration":0.069181,"end_time":"2026-08-05T17:51:28.515923+00:00","exception":false,"start_time":"2026-08-05T17:51:28.446742+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"2db9c12d","cell_type":"code","source":"def read_dicom_slices(root, study_uid, series_uid, n_slices=N_SLICES):\n    files = sorted((root / str(study_uid) / str(series_uid)).glob('*.dcm'))\n    if not files:\n        raise FileNotFoundError(f'No DICOMs for {study_uid}/{series_uid}')\n    take = np.linspace(0, len(files) - 1, n_slices).round().astype(int)\n    images = []\n    for i in take:\n        x = pydicom.dcmread(files[i]).pixel_array.astype(np.float32)\n        lo, hi = np.percentile(x, (1, 99))\n        x = np.clip((x - lo) / max(hi - lo, 1e-6), 0, 1)\n        x = torch.from_numpy(x)[None, None]\n        x = nn.functional.interpolate(x, size=(IMAGE_SIZE, IMAGE_SIZE), mode='bilinear', align_corners=False)[0, 0]\n        images.append(x)\n    return torch.stack(images).float()\n\nclass KneeDataset(Dataset):\n    def __init__(self, frame, root, labels=None):\n        self.frame, self.root, self.labels = frame.reset_index(drop=True), Path(root), labels\n    def __len__(self): return len(self.frame)\n    def __getitem__(self, idx):\n        row = self.frame.iloc[idx]\n        x = read_dicom_slices(self.root, row[ID_COL], row.SeriesInstanceUID)\n        if self.labels is None: return x, row[ID_COL]\n        y = torch.tensor(row[self.labels].to_numpy(dtype=np.float32))\n        return x, y\n\nx, y = KneeDataset(train_labeled.iloc[:1], DATA_DIR / 'train_series', LABELS)[0]\nprint('decoded tensor:', tuple(x.shape), '| target:', y.tolist())","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:28.521288Z","iopub.status.busy":"2026-08-05T17:51:28.520855Z","iopub.status.idle":"2026-08-05T17:51:28.72468Z","shell.execute_reply":"2026-08-05T17:51:28.723673Z"},"papermill":{"duration":0.208469,"end_time":"2026-08-05T17:51:28.72644+00:00","exception":false,"start_time":"2026-08-05T17:51:28.517971+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b9e57e97","cell_type":"code","source":"any_positive = train_labeled[LABELS].max(axis=1)\ntr_idx, va_idx = train_test_split(np.arange(len(train_labeled)), test_size=0.15, random_state=SEED, stratify=any_positive)\ntr_df, va_df = train_labeled.iloc[tr_idx], train_labeled.iloc[va_idx]\ntrain_loader = DataLoader(KneeDataset(tr_df, DATA_DIR / 'train_series', LABELS), batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, pin_memory=True)\nvalid_loader = DataLoader(KneeDataset(va_df, DATA_DIR / 'train_series', LABELS), batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\n\nclass Small2p5DNet(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.backbone = nn.Sequential(\n            nn.Conv2d(in_ch, 48, 5, stride=2, padding=2), nn.BatchNorm2d(48), nn.SiLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(48, 96, 3, stride=2, padding=1), nn.BatchNorm2d(96), nn.SiLU(),\n            nn.Conv2d(96, 192, 3, stride=2, padding=1), nn.BatchNorm2d(192), nn.SiLU(),\n            nn.AdaptiveAvgPool2d(1))\n        self.head = nn.Linear(192, out_ch)\n    def forward(self, x): return self.head(self.backbone(x).flatten(1))\n\nmodel = Small2p5DNet(N_SLICES, len(LABELS)).to(DEVICE)\npos = tr_df[LABELS].sum().to_numpy()\npos_weight = torch.tensor((len(tr_df) - pos) / np.maximum(pos, 1), dtype=torch.float32, device=DEVICE).clamp(max=20)\ncriterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\noptimizer = torch.optim.AdamW(model.parameters(), lr=2e-3, weight_decay=1e-4)\nprint('positive-class weights:', dict(zip(LABELS, pos_weight.cpu().numpy().round(1))))","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:28.733324Z","iopub.status.busy":"2026-08-05T17:51:28.732648Z","iopub.status.idle":"2026-08-05T17:51:34.570069Z","shell.execute_reply":"2026-08-05T17:51:34.568882Z"},"papermill":{"duration":5.842645,"end_time":"2026-08-05T17:51:34.571746+00:00","exception":false,"start_time":"2026-08-05T17:51:28.729101+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"8ceba6a3","cell_type":"code","source":"def predict(loader):\n    model.eval(); out = []\n    with torch.no_grad():\n        for x, _ in loader:\n            out.append(torch.sigmoid(model(x.to(DEVICE, dtype=torch.float32))).cpu().numpy())\n    return np.concatenate(out)\n\nbest_auc, best_state = -np.inf, None\nfor epoch in range(EPOCHS):\n    model.train(); losses = []\n    for x, y in train_loader:\n        optimizer.zero_grad(set_to_none=True)\n        loss = criterion(model(x.to(DEVICE, dtype=torch.float32)), y.to(DEVICE, dtype=torch.float32))\n        loss.backward(); optimizer.step(); losses.append(loss.item())\n    p = predict(valid_loader)\n    y = va_df[LABELS].to_numpy()\n    per_label = [roc_auc_score(y[:, i], p[:, i]) if len(np.unique(y[:, i])) == 2 else np.nan for i in range(len(LABELS))]\n    score = np.nanmean(per_label)\n    print(f'epoch {epoch + 1}/{EPOCHS}  loss={np.mean(losses):.4f}  macro_auc={score:.4f}')\n    if score > best_auc:\n        best_auc, best_state = score, {k: v.cpu().clone() for k, v in model.state_dict().items()}\n\nmodel.load_state_dict(best_state)\npd.DataFrame({'label': LABELS, 'auc': per_label}).sort_values('auc', ascending=False)","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:34.577613Z","iopub.status.busy":"2026-08-05T17:51:34.577044Z","iopub.status.idle":"2026-08-05T17:51:43.303516Z","shell.execute_reply":"2026-08-05T17:51:43.302554Z"},"papermill":{"duration":8.731188,"end_time":"2026-08-05T17:51:43.305123+00:00","exception":false,"start_time":"2026-08-05T17:51:34.573935+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"0786cc4c","cell_type":"code","source":"test_loader = DataLoader(KneeDataset(test_data, DATA_DIR / 'test_series'), batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, pin_memory=True)\ntest_pred = predict(test_loader)\nsubmission = pd.DataFrame(test_pred, columns=LABELS)\nsubmission.insert(0, ID_COL, test_data[ID_COL].values)\nsubmission.to_csv('submission.csv', index=False)\nassert submission.columns.tolist() == sample_sub.columns.tolist()\nassert len(submission) == len(test) and ((submission[LABELS] >= 0) & (submission[LABELS] <= 1)).all().all()\ndisplay(submission.head())\nprint('Wrote submission.csv:', submission.shape)","metadata":{"execution":{"iopub.execute_input":"2026-08-05T17:51:43.312118Z","iopub.status.busy":"2026-08-05T17:51:43.311749Z","iopub.status.idle":"2026-08-05T17:51:44.131441Z","shell.execute_reply":"2026-08-05T17:51:44.13054Z"},"papermill":{"duration":0.824964,"end_time":"2026-08-05T17:51:44.13312+00:00","exception":false,"start_time":"2026-08-05T17:51:43.308156+00:00","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}