{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Introduction\n\nKnee magnetic resonance imaging (MRI) is an important imaging modality for evaluating a wide range of musculoskeletal abnormalities, including ligament injuries, meniscal lesions, osteoarthritis, joint effusion, synovitis, contusions, and fractures. Accurate interpretation of knee MRI requires detailed assessment of multiple anatomical structures and imaging sequences, making the process time-consuming and dependent on specialist expertise. The increasing volume of MRI examinations and variability in access to musculoskeletal radiology expertise highlight the need for reliable computer-assisted methods that can support image interpretation.\n\nRecent advances in machine learning and deep learning have enabled automated extraction of clinically relevant features from medical images. Convolutional neural networks can learn complex imaging patterns directly from MRI data and have demonstrated potential for abnormality detection and classification. However, MRI interpretation is not based solely on visual information. Radiology reports contain contextual and diagnostic information that describes the findings identified by clinicians. Integrating imaging and textual information therefore provides a potential multimodal learning framework for automated abnormality detection.\n\nThe RSNA Knee Abnormality Detection dataset provides an opportunity to investigate this approach using knee MRI studies paired with radiology reports and 12 clinically relevant abnormality labels. The abnormalities include ACL and MCL abnormalities, medial and lateral meniscal abnormalities, medial, lateral and patellofemoral osteoarthritis, effusion, synovitis, Baker's cyst, contusion, and fracture. The dataset also presents important methodological challenges, including multiple MRI series per study, variation in image acquisition, class imbalance, and a limited number of fully labeled studies.\n\nTherefore, this study aims to develop and evaluate MRI-based, text-based, and multimodal machine-learning models for automated detection of clinically important knee MRI abnormalities. Model performance will be assessed primarily using macro-averaged ROC-AUC, with additional evaluation using PR-AUC, confidence intervals, and abnormality-specific performance. Comparing MRI-only, text-only, and multimodal approaches will help determine the contribution of imaging and report-derived information to automated abnormality detection.\n\nThe rationale for this work is to investigate whether multimodal artificial intelligence can provide rapid and consistent decision-support information for knee MRI interpretation while addressing challenges associated with variation in abnormality prevalence and limited specialist availability. The findings may provide a foundation for further development and independent validation of AI-assisted knee MRI interpretation systems.","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# IMPORT LIBRARIES\n# ============================================================\n\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom scipy.stats import binomtest\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.impute import SimpleImputer\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.multiclass import OneVsRestClassifier\n\nfrom sklearn.metrics import (\n    roc_auc_score,\n    average_precision_score,\n    accuracy_score,\n    confusion_matrix,\n    classification_report\n)\n\n\n# ============================================================\n# CONFIGURATION\n# ============================================================\n\nRANDOM_STATE = 42\n\n# Kaggle competition dataset\nDATA_DIR = Path(\n    \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n)\n\n\n# ============================================================\n# DATASET PATHS\n# ============================================================\n\nTRAIN_CSV = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv\"\nTRAIN_SERIES_CSV = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series.csv\"\n\nTEST_CSV = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/test.csv\"\nTEST_SERIES_CSV = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series.csv\"\n\nSAMPLE_SUBMISSION_CSV = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv\"\n\nTRAIN_SERIES_DIR = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series\"\nTEST_SERIES_DIR = DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series\"\n\n\n# ============================================================\n# CHECK DATASET\n# ============================================================\n\nprint(\"=\" * 60)\nprint(\"CHECKING KAGGLE DATASET\")\nprint(\"=\" * 60)\n\nrequired_files = {\n    \"train.csv\": TRAIN_CSV,\n    \"train_series.csv\": TRAIN_SERIES_CSV,\n    \"test.csv\": TEST_CSV,\n    \"test_series.csv\": TEST_SERIES_DIR,\n    \"sample_submission.csv\": SAMPLE_SUBMISSION_CSV,\n}\n\nfor name, path in required_files.items():\n\n    if path.exists():\n        print(f\"✓ Found    {name}\")\n    else:\n        print(f\"✗ Missing  {name}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:33:44.80672Z","iopub.execute_input":"2026-09-19T09:33:44.807118Z","iopub.status.idle":"2026-09-19T09:33:47.297534Z","shell.execute_reply.started":"2026-09-19T09:33:44.807078Z","shell.execute_reply":"2026-09-19T09:33:47.296547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# LOAD DATASETS\n# ============================================================\n\ntrain = pd.read_csv(TRAIN_CSV)\ntrain_series = pd.read_csv(TRAIN_SERIES_CSV)\n\ntest = pd.read_csv(TEST_CSV)\ntest_series = pd.read_csv(TEST_SERIES_CSV)\n\nsample_submission = pd.read_csv(SAMPLE_SUBMISSION_CSV)\n\n\n# ============================================================\n# DATASET SHAPES\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"DATASET SHAPES\")\nprint(\"=\" * 60)\n\nprint(f\"train.csv:             {train.shape}\")\nprint(f\"train_series.csv:      {train_series.shape}\")\nprint(f\"test.csv:              {test.shape}\")\nprint(f\"test_series.csv:       {test_series.shape}\")\nprint(f\"sample_submission.csv: {sample_submission.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:33:57.777155Z","iopub.execute_input":"2026-09-19T09:33:57.777505Z","iopub.status.idle":"2026-09-19T09:33:58.021302Z","shell.execute_reply.started":"2026-09-19T09:33:57.777476Z","shell.execute_reply":"2026-09-19T09:33:58.020534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# DICOM DATASET PATHS\n# ============================================================\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"DICOM DATASET\")\nprint(\"=\" * 60)\n\nprint(\"Dataset directory:\")\nprint(DATA_DIR)\n\nprint(\"\\nTrain series directory:\")\nprint(TRAIN_SERIES_DIR)\n\nprint(\"\\nTest series directory:\")\nprint(TEST_SERIES_DIR)\n\nprint(\"\\nTrain series exists:\", TRAIN_SERIES_DIR.exists())\nprint(\"Test series exists:\", TEST_SERIES_DIR.exists())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:02.847799Z","iopub.execute_input":"2026-09-19T09:34:02.84812Z","iopub.status.idle":"2026-09-19T09:34:02.854566Z","shell.execute_reply.started":"2026-09-19T09:34:02.84809Z","shell.execute_reply":"2026-09-19T09:34:02.853795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nprint(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n\nfor p in Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\").iterdir():\n    print(p)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:08.799119Z","iopub.execute_input":"2026-09-19T09:34:08.799473Z","iopub.status.idle":"2026-09-19T09:34:08.806167Z","shell.execute_reply.started":"2026-09-19T09:34:08.799445Z","shell.execute_reply":"2026-09-19T09:34:08.8054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport pydicom\nimport torch\nimport torch.nn as nn\n\nfrom pathlib import Path\nfrom torchvision import models, transforms\nfrom PIL import Image\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import roc_auc_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:11.793545Z","iopub.execute_input":"2026-09-19T09:34:11.793877Z","iopub.status.idle":"2026-09-19T09:34:18.716512Z","shell.execute_reply.started":"2026-09-19T09:34:11.793848Z","shell.execute_reply":"2026-09-19T09:34:18.715776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nDATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n\nprint(\"Exists:\", DATA_DIR.exists())\nprint(\"Is directory:\", DATA_DIR.is_dir())\n\nif DATA_DIR.exists():\n    print(\"\\nContents:\")\n    for p in DATA_DIR.iterdir():\n        print(p)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:21.504574Z","iopub.execute_input":"2026-09-19T09:34:21.505522Z","iopub.status.idle":"2026-09-19T09:34:21.512942Z","shell.execute_reply.started":"2026-09-19T09:34:21.50549Z","shell.execute_reply":"2026-09-19T09:34:21.512076Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_SERIES_DIR = DATA_DIR / \"train_series\"\nTEST_SERIES_DIR = DATA_DIR / \"test_series\"\n\nprint(\"Train series:\", TRAIN_SERIES_DIR.exists())\nprint(\"Test series:\", TEST_SERIES_DIR.exists())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:24.633378Z","iopub.execute_input":"2026-09-19T09:34:24.633687Z","iopub.status.idle":"2026-09-19T09:34:24.639076Z","shell.execute_reply.started":"2026-09-19T09:34:24.63366Z","shell.execute_reply":"2026-09-19T09:34:24.63818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom pathlib import Path\n\n# Load metadata\ntrain = pd.read_csv(DATA_DIR / \"train.csv\")\ntrain_series = pd.read_csv(DATA_DIR / \"train_series.csv\")\n\nTARGET_COLS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\n# Keep the 58 fully labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\nprint(\"Total studies:\", len(train))\nprint(\"Fully labeled studies:\", len(labeled))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:27.181661Z","iopub.execute_input":"2026-09-19T09:34:27.181986Z","iopub.status.idle":"2026-09-19T09:34:27.36994Z","shell.execute_reply.started":"2026-09-19T09:34:27.181958Z","shell.execute_reply":"2026-09-19T09:34:27.369124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_uid = labeled.iloc[0][\"StudyInstanceUID\"]\n\nprint(\"Selected StudyInstanceUID:\")\nprint(study_uid)\n\nstudy_series = train_series[\n    train_series[\"StudyInstanceUID\"] == study_uid\n].copy()\n\nprint(\"\\nNumber of series:\", len(study_series))\n\ndisplay(study_series)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:29.929724Z","iopub.execute_input":"2026-09-19T09:34:29.930416Z","iopub.status.idle":"2026-09-19T09:34:29.958023Z","shell.execute_reply.started":"2026-09-19T09:34:29.930385Z","shell.execute_reply":"2026-09-19T09:34:29.957128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_dir = TRAIN_SERIES_DIR / study_uid\n\nprint(\"Study directory:\")\nprint(study_dir)\n\nfor series_uid in study_series[\"SeriesInstanceUID\"]:\n    series_dir = study_dir / series_uid\n    dcm_files = list(series_dir.glob(\"*.dcm\"))\n\n    print(\"\\nSeries:\", series_uid)\n    print(\"DICOM slices:\", len(dcm_files))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:33.297159Z","iopub.execute_input":"2026-09-19T09:34:33.298136Z","iopub.status.idle":"2026-09-19T09:34:33.34059Z","shell.execute_reply.started":"2026-09-19T09:34:33.298103Z","shell.execute_reply":"2026-09-19T09:34:33.339726Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pydicom\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n# Select the first series\nseries_uid = study_series.iloc[0][\"SeriesInstanceUID\"]\nseries_dir = study_dir / series_uid\n\ndcm_files = list(series_dir.glob(\"*.dcm\"))\n\nprint(\"Series UID:\", series_uid)\nprint(\"Number of DICOM files:\", len(dcm_files))\n\n# Read all slices\nslices = []\n\nfor f in dcm_files:\n    ds = pydicom.dcmread(f)\n    slices.append(ds)\n\nprint(\"Successfully read:\", len(slices))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:36.251655Z","iopub.execute_input":"2026-09-19T09:34:36.25199Z","iopub.status.idle":"2026-09-19T09:34:36.527459Z","shell.execute_reply.started":"2026-09-19T09:34:36.25196Z","shell.execute_reply":"2026-09-19T09:34:36.526665Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ds = slices[0]\n\nprint(\"Rows:\", ds.Rows)\nprint(\"Columns:\", ds.Columns)\nprint(\"Pixel Spacing:\", getattr(ds, \"PixelSpacing\", \"Not available\"))\nprint(\"Slice Thickness:\", getattr(ds, \"SliceThickness\", \"Not available\"))\nprint(\"Image Orientation:\", getattr(ds, \"ImageOrientationPatient\", \"Not available\"))\nprint(\"Image Position:\", getattr(ds, \"ImagePositionPatient\", \"Not available\"))\nprint(\"Instance Number:\", getattr(ds, \"InstanceNumber\", \"Not available\"))\nprint(\"Modality:\", getattr(ds, \"Modality\", \"Not available\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:39.008429Z","iopub.execute_input":"2026-09-19T09:34:39.009002Z","iopub.status.idle":"2026-09-19T09:34:39.015889Z","shell.execute_reply.started":"2026-09-19T09:34:39.008971Z","shell.execute_reply":"2026-09-19T09:34:39.015024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ndef slice_position(ds):\n    \"\"\"\n    Calculate the spatial position of a DICOM slice.\n    \"\"\"\n    orientation = np.array(ds.ImageOrientationPatient, dtype=float)\n\n    row_cosines = orientation[:3]\n    col_cosines = orientation[3:]\n\n    normal = np.cross(row_cosines, col_cosines)\n\n    position = np.array(ds.ImagePositionPatient, dtype=float)\n\n    return np.dot(position, normal)\n\n\n# Sort slices by spatial position\nslices_sorted = sorted(slices, key=slice_position)\n\nprint(\"First slice position:\",\n      slice_position(slices_sorted[0]))\n\nprint(\"Last slice position:\",\n      slice_position(slices_sorted[-1]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:42.497452Z","iopub.execute_input":"2026-09-19T09:34:42.498108Z","iopub.status.idle":"2026-09-19T09:34:42.511726Z","shell.execute_reply.started":"2026-09-19T09:34:42.498076Z","shell.execute_reply":"2026-09-19T09:34:42.510784Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The MRI series comprised 36 slices with spatial positions ranging from 64.17 to 164.79 mm, corresponding to approximately 100.62 mm of anatomical coverage. The slices were spatially ordered using DICOM positional information before reconstruction of the 3-D volume.","metadata":{}},{"cell_type":"code","source":"volume = np.stack(\n    [ds.pixel_array.astype(np.float32) for ds in slices_sorted],\n    axis=0\n)\n\nprint(\"3-D volume shape:\", volume.shape)\nprint(\"Data type:\", volume.dtype)\nprint(\"Minimum:\", volume.min())\nprint(\"Maximum:\", volume.max())\nprint(\"Mean:\", volume.mean())\nprint(\"SD:\", volume.std())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:45.188725Z","iopub.execute_input":"2026-09-19T09:34:45.189564Z","iopub.status.idle":"2026-09-19T09:34:45.278305Z","shell.execute_reply.started":"2026-09-19T09:34:45.189527Z","shell.execute_reply":"2026-09-19T09:34:45.277505Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CORRELATION HEATMAP OF KNEE ABNORMALITIES\n# ============================================================\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Use only fully labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\n# Calculate correlation matrix\ncorr_matrix = labeled[TARGET_COLS].corr()\n\n# Plot heatmap\nplt.figure(figsize=(12, 9))\n\nsns.heatmap(\n    corr_matrix,\n    annot=True,\n    fmt=\".2f\",\n    cmap=\"coolwarm\",\n    center=0,\n    vmin=-1,\n    vmax=1,\n    linewidths=0.5,\n    square=True\n)\n\nplt.title(\"Correlation Heatmap of Knee Abnormalities\")\nplt.xticks(rotation=45, ha=\"right\")\nplt.yticks(rotation=0)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:47.70526Z","iopub.execute_input":"2026-09-19T09:34:47.706451Z","iopub.status.idle":"2026-09-19T09:34:48.510813Z","shell.execute_reply.started":"2026-09-19T09:34:47.706392Z","shell.execute_reply":"2026-09-19T09:34:48.50992Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(11, 8))\n\nmask = np.triu(np.ones_like(corr_matrix, dtype=bool))\n\nsns.heatmap(\n    corr_matrix,\n    mask=mask,\n    annot=True,\n    fmt=\".2f\",\n    cmap=\"RdBu_r\",\n    center=0,\n    vmin=-1,\n    vmax=1,\n    linewidths=0.5\n)\n\nplt.title(\"Correlation Between Knee MRI Abnormalities\")\nplt.xlabel(\"Abnormality\")\nplt.ylabel(\"Abnormality\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:55.839764Z","iopub.execute_input":"2026-09-19T09:34:55.840631Z","iopub.status.idle":"2026-09-19T09:34:56.289763Z","shell.execute_reply.started":"2026-09-19T09:34:55.8406Z","shell.execute_reply":"2026-09-19T09:34:56.289013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CO-OCCURRENCE OF KNEE MRI ABNORMALITIES\n# ============================================================\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Use only fully labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\n# Convert labels to integer\ny = labeled[TARGET_COLS].astype(int)\n\n# Calculate co-occurrence matrix\n# Matrix[i,j] = number of studies where both abnormalities are present\nco_occurrence = y.T.dot(y)\n\n# Plot\nplt.figure(figsize=(12, 10))\n\nsns.heatmap(\n    co_occurrence,\n    annot=True,\n    fmt=\"d\",\n    cmap=\"YlOrRd\",\n    square=True,\n    linewidths=0.5,\n    cbar_kws={\"label\": \"Number of studies\"}\n)\n\nplt.title(\n    \"Co-occurrence of Knee MRI Abnormalities\",\n    fontsize=16,\n    fontweight=\"bold\"\n)\n\nplt.xlabel(\"Abnormality\")\nplt.ylabel(\"Abnormality\")\n\nplt.xticks(rotation=45, ha=\"right\")\nplt.yticks(rotation=0)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:34:59.21676Z","iopub.execute_input":"2026-09-19T09:34:59.217102Z","iopub.status.idle":"2026-09-19T09:34:59.765934Z","shell.execute_reply.started":"2026-09-19T09:34:59.217072Z","shell.execute_reply":"2026-09-19T09:34:59.765228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# PREVALENCE WITH 95% CI\n# ============================================================\n\nfrom statsmodels.stats.proportion import proportion_confint\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Use only fully labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\nn = len(labeled)\n\nresults = []\n\nfor abnormality in TARGET_COLS:\n\n    positive = labeled[abnormality].astype(int).sum()\n\n    prevalence = positive / n\n\n    # Wilson 95% CI\n    ci_low, ci_high = proportion_confint(\n        count=positive,\n        nobs=n,\n        alpha=0.05,\n        method=\"wilson\"\n    )\n\n    results.append({\n        \"Abnormality\": abnormality,\n        \"Positive\": positive,\n        \"Total\": n,\n        \"Prevalence\": prevalence,\n        \"95% CI Lower\": ci_low,\n        \"95% CI Upper\": ci_high\n    })\n\nprevalence_df = pd.DataFrame(results)\n\n# Convert to percentage\nprevalence_df[\"Prevalence (%)\"] = prevalence_df[\"Prevalence\"] * 100\nprevalence_df[\"95% CI Lower (%)\"] = prevalence_df[\"95% CI Lower\"] * 100\nprevalence_df[\"95% CI Upper (%)\"] = prevalence_df[\"95% CI Upper\"] * 100\n\n# Display\nprevalence_df[\n    [\n        \"Abnormality\",\n        \"Positive\",\n        \"Total\",\n        \"Prevalence (%)\",\n        \"95% CI Lower (%)\",\n        \"95% CI Upper (%)\"\n    ]\n].round(2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:07.080145Z","iopub.execute_input":"2026-09-19T09:35:07.080953Z","iopub.status.idle":"2026-09-19T09:35:07.154228Z","shell.execute_reply.started":"2026-09-19T09:35:07.08092Z","shell.execute_reply":"2026-09-19T09:35:07.153533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# PREVALENCE PLOT WITH 95% CI\n# ============================================================\n\nplot_df = prevalence_df.sort_values(\n    \"Prevalence (%)\",\n    ascending=True\n)\n\nplt.figure(figsize=(10, 7))\n\nplt.errorbar(\n    plot_df[\"Prevalence (%)\"],\n    plot_df[\"Abnormality\"],\n    xerr=[\n        plot_df[\"Prevalence (%)\"] - plot_df[\"95% CI Lower (%)\"],\n        plot_df[\"95% CI Upper (%)\"] - plot_df[\"Prevalence (%)\"]\n    ],\n    fmt=\"o\",\n    capsize=4\n)\n\nplt.xlabel(\"Prevalence (%)\")\nplt.ylabel(\"Abnormality\")\nplt.title(\"Prevalence of Knee MRI Abnormalities with 95% CI\")\n\nplt.xlim(0, 100)\nplt.grid(axis=\"x\", alpha=0.3)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:11.408288Z","iopub.execute_input":"2026-09-19T09:35:11.409101Z","iopub.status.idle":"2026-09-19T09:35:11.61291Z","shell.execute_reply.started":"2026-09-19T09:35:11.409061Z","shell.execute_reply":"2026-09-19T09:35:11.612134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Among the 58 fully labeled MRI studies, the prevalence of the 12 knee abnormalities varied substantially. Effusion had the highest prevalence, while MCL abnormality had a lower prevalence. The 95% confidence intervals demonstrate the uncertainty surrounding these estimates, particularly for less frequent abnormalities. Because the prevalence estimates are based on only 58 fully labeled studies, they should be interpreted as preliminary estimates rather than population-level prevalence.","metadata":{}},{"cell_type":"markdown","source":"# MRI Volume Characteristics\n\nThe selected MRI series produced a 3-D volume of 36 × 400 × 400 voxels, representing 36 image slices with a spatial resolution of 400 × 400 pixels per slice. The data were stored as 32-bit floating-point values.\n\nThe voxel intensity values ranged from 0 to 1074, with a mean of 173.25 and a standard deviation of 182.29. The relatively large standard deviation indicates substantial variation in image intensity across the volume, which is expected in MRI because different tissues and imaging sequences can produce different signal intensities.","metadata":{}},{"cell_type":"code","source":"p1, p99 = np.percentile(volume, [1, 99])\n\nvolume_norm = np.clip(\n    (volume - p1) / (p99 - p1),\n    0,\n    1\n)\n\nprint(\"Normalized range:\",\n      volume_norm.min(),\n      volume_norm.max())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:15.57692Z","iopub.execute_input":"2026-09-19T09:35:15.577737Z","iopub.status.idle":"2026-09-19T09:35:15.727289Z","shell.execute_reply.started":"2026-09-19T09:35:15.577705Z","shell.execute_reply":"2026-09-19T09:35:15.726492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"middle = volume_norm.shape[0] // 2\n\nplt.figure(figsize=(6, 6))\nplt.imshow(volume_norm[middle], cmap=\"gray\")\nplt.title(f\"Middle Slice — {series_uid}\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:19.111733Z","iopub.execute_input":"2026-09-19T09:35:19.112665Z","iopub.status.idle":"2026-09-19T09:35:19.335944Z","shell.execute_reply.started":"2026-09-19T09:35:19.112624Z","shell.execute_reply":"2026-09-19T09:35:19.335049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Kaggle paths\nDATA_DIR = Path(\"/kaggle/input/competitions/rsna-knee-abnormality-detection\")\n\nTRAIN_SERIES_DIR = DATA_DIR / \"train_series\"\n\ntrain = pd.read_csv(DATA_DIR / \"train.csv\")\ntrain_series = pd.read_csv(DATA_DIR / \"train_series.csv\")\n\nTARGET_COLS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\n# Only completely labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\nprint(\"Labeled studies:\", len(labeled))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:25.880042Z","iopub.execute_input":"2026-09-19T09:35:25.880903Z","iopub.status.idle":"2026-09-19T09:35:26.040827Z","shell.execute_reply.started":"2026-09-19T09:35:25.880872Z","shell.execute_reply":"2026-09-19T09:35:26.040028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def select_series(study_uid):\n    \n    series = train_series[\n        train_series[\"StudyInstanceUID\"] == study_uid\n    ].copy()\n    \n    # Prefer fluid-sensitive series\n    fluid = series[series[\"Fluid_Sensitive\"] == 1]\n    \n    if len(fluid) > 0:\n        series = fluid\n    \n    return series","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:28.58447Z","iopub.execute_input":"2026-09-19T09:35:28.585143Z","iopub.status.idle":"2026-09-19T09:35:28.589864Z","shell.execute_reply.started":"2026-09-19T09:35:28.585113Z","shell.execute_reply":"2026-09-19T09:35:28.589093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_uid = labeled.iloc[0][\"StudyInstanceUID\"]\n\nselected = select_series(study_uid)\n\nprint(selected[\n    [\n        \"SeriesInstanceUID\",\n        \"Fluid_Sensitive\",\n        \"Fat_Suppression\",\n        \"Anatomical_Plane\"\n    ]\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:31.361446Z","iopub.execute_input":"2026-09-19T09:35:31.36175Z","iopub.status.idle":"2026-09-19T09:35:31.373049Z","shell.execute_reply.started":"2026-09-19T09:35:31.361724Z","shell.execute_reply":"2026-09-19T09:35:31.372011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_series(series_dir):\n    \n    dcm_files = list(series_dir.glob(\"*.dcm\"))\n    \n    slices = []\n    \n    for f in dcm_files:\n        try:\n            ds = pydicom.dcmread(f)\n            \n            if not hasattr(ds, \"PixelData\"):\n                continue\n            \n            slices.append(ds)\n            \n        except Exception as e:\n            print(\"Could not read:\", f)\n    \n    if len(slices) == 0:\n        return None\n    \n    # Spatial ordering\n    def position(ds):\n        \n        if hasattr(ds, \"ImageOrientationPatient\") and \\\n           hasattr(ds, \"ImagePositionPatient\"):\n            \n            orientation = np.array(\n                ds.ImageOrientationPatient,\n                dtype=float\n            )\n            \n            row = orientation[:3]\n            col = orientation[3:]\n            \n            normal = np.cross(row, col)\n            \n            pos = np.array(\n                ds.ImagePositionPatient,\n                dtype=float\n            )\n            \n            return np.dot(pos, normal)\n        \n        return getattr(ds, \"InstanceNumber\", 0)\n    \n    slices = sorted(slices, key=position)\n    \n    volume = np.stack(\n        [ds.pixel_array.astype(np.float32)\n         for ds in slices]\n    )\n    \n    # Robust normalization\n    p1, p99 = np.percentile(volume, [1, 99])\n    \n    if p99 > p1:\n        volume = (volume - p1) / (p99 - p1)\n    \n    volume = np.clip(volume, 0, 1)\n    \n    return volume","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:36.98481Z","iopub.execute_input":"2026-09-19T09:35:36.985133Z","iopub.status.idle":"2026-09-19T09:35:36.994345Z","shell.execute_reply.started":"2026-09-19T09:35:36.985106Z","shell.execute_reply":"2026-09-19T09:35:36.993407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def sample_slices(volume, n_slices=16):\n    \n    n = volume.shape[0]\n    \n    indices = np.linspace(\n        0,\n        n - 1,\n        n_slices\n    ).astype(int)\n    \n    return volume[indices]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:42.375434Z","iopub.execute_input":"2026-09-19T09:35:42.375761Z","iopub.status.idle":"2026-09-19T09:35:42.380722Z","shell.execute_reply.started":"2026-09-19T09:35:42.375733Z","shell.execute_reply":"2026-09-19T09:35:42.379949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)),\n    transforms.Grayscale(num_output_channels=3),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        mean=[0.485, 0.456, 0.406],\n        std=[0.229, 0.224, 0.225]\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:44.968177Z","iopub.execute_input":"2026-09-19T09:35:44.969866Z","iopub.status.idle":"2026-09-19T09:35:44.975064Z","shell.execute_reply.started":"2026-09-19T09:35:44.969832Z","shell.execute_reply":"2026-09-19T09:35:44.974395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:00:23.92368Z","iopub.execute_input":"2026-09-17T10:00:23.92404Z","iopub.status.idle":"2026-09-17T10:00:23.955365Z","shell.execute_reply.started":"2026-09-17T10:00:23.924012Z","shell.execute_reply":"2026-09-17T10:00:23.954116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"weights = models.ResNet18_Weights.DEFAULT\n\nmodel = models.resnet18(\n    weights=weights\n)\n\n# Remove final classification layer\nfeature_extractor = nn.Sequential(\n    *list(model.children())[:-1]\n)\n\nfeature_extractor = feature_extractor.to(device)\nfeature_extractor.eval()\n\nfor param in feature_extractor.parameters():\n    param.requires_grad = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:35:47.814677Z","iopub.execute_input":"2026-09-19T09:35:47.815004Z","iopub.status.idle":"2026-09-19T09:36:20.061859Z","shell.execute_reply.started":"2026-09-19T09:35:47.814976Z","shell.execute_reply":"2026-09-19T09:36:20.060174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_features(volume):\n    \n    slices = sample_slices(volume, n_slices=16)\n    \n    batch = torch.stack([\n        transform((s * 255).astype(np.uint8))\n        for s in slices\n    ])\n    \n    batch = batch.to(device)\n    \n    with torch.no_grad():\n        features = feature_extractor(batch)\n    \n    # 16 slices × 512 features\n    features = features.squeeze(-1).squeeze(-1)\n    \n    # Aggregate slices\n    study_feature = features.mean(dim=0)\n    \n    return study_feature.cpu().numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:36:20.063967Z","iopub.status.idle":"2026-09-19T09:36:20.064257Z","shell.execute_reply.started":"2026-09-19T09:36:20.064119Z","shell.execute_reply":"2026-09-19T09:36:20.064136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X = []\nY = []\nstudy_ids = []\n\nfor i, row in labeled.iterrows():\n    \n    study_uid = row[\"StudyInstanceUID\"]\n    \n    print(\n        f\"Processing {len(X)+1}/{len(labeled)}:\",\n        study_uid\n    )\n    \n    series_info = select_series(study_uid)\n    \n    series_features = []\n    \n    for _, srow in series_info.iterrows():\n        \n        series_uid = srow[\"SeriesInstanceUID\"]\n        \n        series_dir = (\n            TRAIN_SERIES_DIR /\n            study_uid /\n            series_uid\n        )\n        \n        volume = load_series(series_dir)\n        \n        if volume is None:\n            continue\n        \n        feature = extract_features(volume)\n        \n        series_features.append(feature)\n    \n    if len(series_features) == 0:\n        print(\"No usable series:\", study_uid)\n        continue\n    \n    # Aggregate multiple series\n    study_feature = np.mean(\n        series_features,\n        axis=0\n    )\n    \n    X.append(study_feature)\n    Y.append(row[TARGET_COLS].values.astype(float))\n    study_ids.append(study_uid)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:36:20.065889Z","iopub.status.idle":"2026-09-19T09:36:20.0662Z","shell.execute_reply.started":"2026-09-19T09:36:20.066053Z","shell.execute_reply":"2026-09-19T09:36:20.066071Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X = np.array(X)\nY = np.array(Y)\n\nprint(\"Feature matrix:\", X.shape)\nprint(\"Label matrix:\", Y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:36:20.067186Z","iopub.status.idle":"2026-09-19T09:36:20.067634Z","shell.execute_reply.started":"2026-09-19T09:36:20.067412Z","shell.execute_reply":"2026-09-19T09:36:20.06744Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.linear_model import LogisticRegression\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.pipeline import Pipeline","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:58.424538Z","iopub.execute_input":"2026-09-17T10:03:58.424976Z","iopub.status.idle":"2026-09-17T10:03:58.45145Z","shell.execute_reply.started":"2026-09-17T10:03:58.424931Z","shell.execute_reply":"2026-09-17T10:03:58.450223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\noof_predictions = np.zeros_like(Y)\n\nfor target_idx, target in enumerate(TARGET_COLS):\n    \n    y = Y[:, target_idx]\n    \n    for train_idx, valid_idx in kf.split(X):\n        \n        model = Pipeline([\n            (\n                \"scaler\",\n                StandardScaler()\n            ),\n            (\n                \"classifier\",\n                LogisticRegression(\n                    max_iter=2000,\n                    class_weight=\"balanced\"\n                )\n            )\n        ])\n        \n        model.fit(\n            X[train_idx],\n            y[train_idx]\n        )\n        \n        oof_predictions[\n            valid_idx,\n            target_idx\n        ] = model.predict_proba(\n            X[valid_idx]\n        )[:, 1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:58.452617Z","iopub.execute_input":"2026-09-17T10:03:58.452957Z","iopub.status.idle":"2026-09-17T10:03:59.342873Z","shell.execute_reply.started":"2026-09-17T10:03:58.452865Z","shell.execute_reply":"2026-09-17T10:03:59.342073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = []\n\nfor i, target in enumerate(TARGET_COLS):\n    \n    auc = roc_auc_score(\n        Y[:, i],\n        oof_predictions[:, i]\n    )\n    \n    results.append({\n        \"Abnormality\": target,\n        \"MRI_AUC\": auc\n    })\n\nresults_df = pd.DataFrame(results)\n\ndisplay(results_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.343953Z","iopub.execute_input":"2026-09-17T10:03:59.345034Z","iopub.status.idle":"2026-09-17T10:03:59.381357Z","shell.execute_reply.started":"2026-09-17T10:03:59.344991Z","shell.execute_reply":"2026-09-17T10:03:59.38064Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"macro_auc = results_df[\"MRI_AUC\"].mean()\n\nprint(\n    f\"MRI-only Macro ROC-AUC: {macro_auc:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.382223Z","iopub.execute_input":"2026-09-17T10:03:59.382525Z","iopub.status.idle":"2026-09-17T10:03:59.390363Z","shell.execute_reply.started":"2026-09-17T10:03:59.382493Z","shell.execute_reply":"2026-09-17T10:03:59.388397Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\ntrain = pd.read_csv(DATA_DIR / \"/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv\")\n\nTARGET_COLS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\n# Fully labeled studies\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\nprint(\"Labeled studies:\", len(labeled))\nprint(\"Report missing:\", labeled[\"Report\"].isna().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.392408Z","iopub.execute_input":"2026-09-17T10:03:59.394406Z","iopub.status.idle":"2026-09-17T10:03:59.534635Z","shell.execute_reply.started":"2026-09-17T10:03:59.394362Z","shell.execute_reply":"2026-09-17T10:03:59.533586Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_COLS = [\n    \"ACL\", \"MCL\", \"Medial Meniscus\", \"Lateral Meniscus\",\n    \"Medial OA\", \"Lateral OA\", \"PF OA\", \"Effusion\",\n    \"Synovitis\", \"Baker's\", \"Contusion\", \"Fracture\"\n]\n\nlabeled = train.dropna(subset=TARGET_COLS).copy()\n\nprint(\"Labeled studies:\", len(labeled))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.536133Z","iopub.execute_input":"2026-09-17T10:03:59.53641Z","iopub.status.idle":"2026-09-17T10:03:59.547392Z","shell.execute_reply.started":"2026-09-17T10:03:59.536383Z","shell.execute_reply":"2026-09-17T10:03:59.546083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\nimport pandas as pd\n\ndef clean_report(text):\n    if pd.isna(text):\n        return \"\"\n    \n    text = str(text).lower()\n    text = re.sub(r\"\\s+\", \" \", text)\n    return text.strip()\n\nlabeled[\"Report_Clean\"] = labeled[\"Report\"].apply(clean_report)\n\nprint(\"Report_Clean created:\", \"Report_Clean\" in labeled.columns)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.548979Z","iopub.execute_input":"2026-09-17T10:03:59.549389Z","iopub.status.idle":"2026-09-17T10:03:59.577187Z","shell.execute_reply.started":"2026-09-17T10:03:59.549357Z","shell.execute_reply":"2026-09-17T10:03:59.576189Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled[\"Report_Length\"] = labeled[\"Report_Clean\"].str.len()\n\nprint(labeled[\"Report_Length\"].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.578509Z","iopub.execute_input":"2026-09-17T10:03:59.578921Z","iopub.status.idle":"2026-09-17T10:03:59.609528Z","shell.execute_reply.started":"2026-09-17T10:03:59.57886Z","shell.execute_reply":"2026-09-17T10:03:59.608562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Missing reports:\", (labeled[\"Report_Clean\"] == \"\").sum())\nprint(\"Non-missing reports:\", (labeled[\"Report_Clean\"] != \"\").sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.611101Z","iopub.execute_input":"2026-09-17T10:03:59.611496Z","iopub.status.idle":"2026-09-17T10:03:59.634768Z","shell.execute_reply.started":"2026-09-17T10:03:59.611455Z","shell.execute_reply":"2026-09-17T10:03:59.633646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i in range(min(5, len(labeled))):\n    \n    print(\"\\n\" + \"=\"*80)\n    print(labeled[\"Report_Clean\"].iloc[i][:1500])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.636011Z","iopub.execute_input":"2026-09-17T10:03:59.636501Z","iopub.status.idle":"2026-09-17T10:03:59.664149Z","shell.execute_reply.started":"2026-09-17T10:03:59.636459Z","shell.execute_reply":"2026-09-17T10:03:59.662861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.feature_extraction.text import TfidfVectorizer\n\nvectorizer = TfidfVectorizer(\n    ngram_range=(1, 2),\n    min_df=2,\n    max_df=0.95,\n    sublinear_tf=True,\n    max_features=10000\n)\n\nX_text = vectorizer.fit_transform(\n    labeled[\"Report_Clean\"]\n)\n\nprint(\"TF-IDF shape:\", X_text.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.665712Z","iopub.execute_input":"2026-09-17T10:03:59.666322Z","iopub.status.idle":"2026-09-17T10:03:59.734352Z","shell.execute_reply.started":"2026-09-17T10:03:59.66626Z","shell.execute_reply":"2026-09-17T10:03:59.733377Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"Y_text = (\n    labeled[TARGET_COLS]\n    .astype(int)\n    .values\n)\n\nprint(\"Text feature matrix:\", X_text.shape)\nprint(\"Label matrix:\", Y_text.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.735798Z","iopub.execute_input":"2026-09-17T10:03:59.736856Z","iopub.status.idle":"2026-09-17T10:03:59.744333Z","shell.execute_reply.started":"2026-09-17T10:03:59.736824Z","shell.execute_reply":"2026-09-17T10:03:59.743024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import roc_auc_score\n\nkf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\ntext_oof = np.zeros_like(\n    Y_text,\n    dtype=float\n)\n\nfor target_idx, target in enumerate(TARGET_COLS):\n    \n    y = Y_text[:, target_idx]\n    \n    for train_idx, valid_idx in kf.split(X_text):\n        \n        model = LogisticRegression(\n            max_iter=3000,\n            class_weight=\"balanced\"\n        )\n        \n        model.fit(\n            X_text[train_idx],\n            y[train_idx]\n        )\n        \n        text_oof[\n            valid_idx,\n            target_idx\n        ] = model.predict_proba(\n            X_text[valid_idx]\n        )[:, 1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:59.746462Z","iopub.execute_input":"2026-09-17T10:03:59.74727Z","iopub.status.idle":"2026-09-17T10:04:00.246287Z","shell.execute_reply.started":"2026-09-17T10:03:59.747237Z","shell.execute_reply":"2026-09-17T10:04:00.245484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"text_results = []\n\nfor i, target in enumerate(TARGET_COLS):\n    \n    auc = roc_auc_score(\n        Y_text[:, i],\n        text_oof[:, i]\n    )\n    \n    text_results.append({\n        \"Abnormality\": target,\n        \"Text_AUC\": auc\n    })\n\ntext_results_df = pd.DataFrame(\n    text_results\n)\n\ndisplay(text_results_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:00.247549Z","iopub.execute_input":"2026-09-17T10:04:00.247865Z","iopub.status.idle":"2026-09-17T10:04:00.29091Z","shell.execute_reply.started":"2026-09-17T10:04:00.247834Z","shell.execute_reply":"2026-09-17T10:04:00.290144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"text_macro_auc = (\n    text_results_df[\"Text_AUC\"].mean()\n)\n\nprint(\n    f\"Report/NLP Macro ROC-AUC: \"\n    f\"{text_macro_auc:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:00.293341Z","iopub.execute_input":"2026-09-17T10:04:00.293671Z","iopub.status.idle":"2026-09-17T10:04:00.302433Z","shell.execute_reply.started":"2026-09-17T10:04:00.29364Z","shell.execute_reply":"2026-09-17T10:04:00.301576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.decomposition import TruncatedSVD\n\nsvd = TruncatedSVD(\n    n_components=50,\n    random_state=42\n)\n\nX_text_reduced = svd.fit_transform(X_text)\n\nprint(\"MRI features:\", X.shape)\nprint(\"Text features:\", X_text_reduced.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:00.304503Z","iopub.execute_input":"2026-09-17T10:04:00.304816Z","iopub.status.idle":"2026-09-17T10:04:00.377702Z","shell.execute_reply.started":"2026-09-17T10:04:00.304786Z","shell.execute_reply":"2026-09-17T10:04:00.376432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X_multimodal = np.hstack([\n    X,\n    X_text_reduced\n])\n\nprint(\"Multimodal feature matrix:\", X_multimodal.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:00.380874Z","iopub.execute_input":"2026-09-17T10:04:00.381249Z","iopub.status.idle":"2026-09-17T10:04:00.406246Z","shell.execute_reply.started":"2026-09-17T10:04:00.381216Z","shell.execute_reply":"2026-09-17T10:04:00.405222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\nfrom sklearn.linear_model import LogisticRegression\nimport numpy as np\n\nkf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nmultimodal_oof = np.zeros(\n    (len(Y), len(TARGET_COLS))\n)\n\nfor target_idx, target in enumerate(TARGET_COLS):\n\n    y = Y[:, target_idx]\n\n    for train_idx, valid_idx in kf.split(X_multimodal):\n\n        model = LogisticRegression(\n            max_iter=3000,\n            class_weight=\"balanced\"\n        )\n\n        model.fit(\n            X_multimodal[train_idx],\n            y[train_idx]\n        )\n\n        multimodal_oof[\n            valid_idx,\n            target_idx\n        ] = model.predict_proba(\n            X_multimodal[valid_idx]\n        )[:, 1]\n\nprint(\n    \"OOF prediction shape:\",\n    multimodal_oof.shape\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:00.407337Z","iopub.execute_input":"2026-09-17T10:04:00.407666Z","iopub.status.idle":"2026-09-17T10:04:02.56174Z","shell.execute_reply.started":"2026-09-17T10:04:00.407635Z","shell.execute_reply":"2026-09-17T10:04:02.560926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multimodal_results = []\n\nfor i, target in enumerate(TARGET_COLS):\n\n    auc = roc_auc_score(\n        Y[:, i],\n        multimodal_oof[:, i]\n    )\n\n    multimodal_results.append({\n        \"Abnormality\": target,\n        \"Multimodal_AUC\": auc\n    })\n\nmultimodal_results_df = pd.DataFrame(\n    multimodal_results\n)\n\ndisplay(multimodal_results_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.563952Z","iopub.execute_input":"2026-09-17T10:04:02.564291Z","iopub.status.idle":"2026-09-17T10:04:02.6097Z","shell.execute_reply.started":"2026-09-17T10:04:02.564258Z","shell.execute_reply":"2026-09-17T10:04:02.608976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multimodal_macro_auc = (\n    multimodal_results_df[\"Multimodal_AUC\"].mean()\n)\n\nprint(\n    f\"Multimodal Macro ROC-AUC: \"\n    f\"{multimodal_macro_auc:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.610556Z","iopub.execute_input":"2026-09-17T10:04:02.610852Z","iopub.status.idle":"2026-09-17T10:04:02.621543Z","shell.execute_reply.started":"2026-09-17T10:04:02.610822Z","shell.execute_reply":"2026-09-17T10:04:02.620691Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"comparison = pd.DataFrame({\n    \"Abnormality\": TARGET_COLS,\n    \"MRI_AUC\": results_df[\"MRI_AUC\"].values,\n    \"Text_AUC\": text_results_df[\"Text_AUC\"].values,\n    \"Multimodal_AUC\": multimodal_results_df[\"Multimodal_AUC\"].values\n})\n\ndisplay(comparison)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.624148Z","iopub.execute_input":"2026-09-17T10:04:02.624492Z","iopub.status.idle":"2026-09-17T10:04:02.654695Z","shell.execute_reply.started":"2026-09-17T10:04:02.624461Z","shell.execute_reply":"2026-09-17T10:04:02.653819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"MRI-only Macro AUC:       \", round(results_df[\"MRI_AUC\"].mean(), 4))\nprint(\"Text-only Macro AUC:      \", round(text_results_df[\"Text_AUC\"].mean(), 4))\nprint(\"Multimodal Macro AUC:     \", round(multimodal_results_df[\"Multimodal_AUC\"].mean(), 4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.656133Z","iopub.execute_input":"2026-09-17T10:04:02.656632Z","iopub.status.idle":"2026-09-17T10:04:02.674587Z","shell.execute_reply.started":"2026-09-17T10:04:02.656599Z","shell.execute_reply":"2026-09-17T10:04:02.673704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"comparison = pd.DataFrame({\n    \"Abnormality\": TARGET_COLS,\n    \"MRI_AUC\": results_df[\"MRI_AUC\"].values,\n    \"Text_AUC\": text_results_df[\"Text_AUC\"].values,\n    \"Multimodal_AUC\": multimodal_results_df[\"Multimodal_AUC\"].values\n})\n\ndisplay(comparison)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.675844Z","iopub.execute_input":"2026-09-17T10:04:02.676462Z","iopub.status.idle":"2026-09-17T10:04:02.702315Z","shell.execute_reply.started":"2026-09-17T10:04:02.676425Z","shell.execute_reply":"2026-09-17T10:04:02.701233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"MRI-only Macro AUC:      \",\n      round(comparison[\"MRI_AUC\"].mean(), 4))\n\nprint(\"Text-only Macro AUC:     \",\n      round(comparison[\"Text_AUC\"].mean(), 4))\n\nprint(\"Multimodal Macro AUC:    \",\n      round(comparison[\"Multimodal_AUC\"].mean(), 4))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.703706Z","iopub.execute_input":"2026-09-17T10:04:02.704112Z","iopub.status.idle":"2026-09-17T10:04:02.726794Z","shell.execute_reply.started":"2026-09-17T10:04:02.704071Z","shell.execute_reply":"2026-09-17T10:04:02.725384Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score\nimport numpy as np\n\ndef macro_auc(y_true, y_pred):\n    aucs = []\n\n    for i in range(y_true.shape[1]):\n        aucs.append(\n            roc_auc_score(\n                y_true[:, i],\n                y_pred[:, i]\n            )\n        )\n\n    return np.mean(aucs)\n\n\ndef bootstrap_macro_auc(\n    y_true,\n    y_pred,\n    n_bootstrap=2000,\n    random_state=42\n):\n    \n    rng = np.random.default_rng(random_state)\n    n = len(y_true)\n\n    scores = []\n\n    for _ in range(n_bootstrap):\n\n        indices = rng.integers(\n            0,\n            n,\n            size=n\n        )\n\n        y_b = y_true[indices]\n        p_b = y_pred[indices]\n\n        # Skip bootstrap samples without both classes\n        valid = True\n\n        for j in range(y_true.shape[1]):\n            if len(np.unique(y_b[:, j])) < 2:\n                valid = False\n                break\n\n        if valid:\n            scores.append(\n                macro_auc(y_b, p_b)\n            )\n\n    scores = np.array(scores)\n\n    lower = np.percentile(scores, 2.5)\n    upper = np.percentile(scores, 97.5)\n\n    return np.mean(scores), lower, upper","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.734299Z","iopub.execute_input":"2026-09-17T10:04:02.734621Z","iopub.status.idle":"2026-09-17T10:04:02.753755Z","shell.execute_reply.started":"2026-09-17T10:04:02.734595Z","shell.execute_reply":"2026-09-17T10:04:02.752507Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"models_to_evaluate = {\n    \"MRI-only\": oof_predictions,\n    \"Text-only\": text_oof,\n    \"Multimodal\": multimodal_oof\n}\n\nfor name, predictions in models_to_evaluate.items():\n\n    mean_auc, lower, upper = bootstrap_macro_auc(\n        Y,\n        predictions,\n        n_bootstrap=2000,\n        random_state=42\n    )\n\n    print(\n        f\"{name}: \"\n        f\"{mean_auc:.4f} \"\n        f\"(95% CI {lower:.4f}–{upper:.4f})\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:02.755006Z","iopub.execute_input":"2026-09-17T10:04:02.755322Z","iopub.status.idle":"2026-09-17T10:05:49.573509Z","shell.execute_reply.started":"2026-09-17T10:04:02.755294Z","shell.execute_reply":"2026-09-17T10:05:49.572421Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Comparison of MRI-only, Text-only, and Multimodal Models\n\nThe MRI-only model achieved a macro-average ROC-AUC of 0.6424 (95% CI: 0.5825–0.6979). The text-only model showed lower performance, with an AUC of 0.5590 (95% CI: 0.5021–0.6139), indicating limited discriminative ability.\n\nThe multimodal model achieved an AUC of 0.6434 (95% CI: 0.5808–0.6994), which was very similar to the MRI-only model. The small difference in AUC (0.0010) suggests that adding report-derived text features provided little additional discrimination in this baseline analysis.","metadata":{}},{"cell_type":"code","source":"mean_auc, lower, upper = bootstrap_macro_auc(\n    Y,\n    multimodal_oof,\n    n_bootstrap=2000,\n    random_state=42\n)\n\nprint(\n    f\"Multimodal: {mean_auc:.4f} \"\n    f\"(95% CI {lower:.4f}–{upper:.4f})\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:05:49.575054Z","iopub.execute_input":"2026-09-17T10:05:49.575546Z","iopub.status.idle":"2026-09-17T10:06:25.237815Z","shell.execute_reply.started":"2026-09-17T10:05:49.575504Z","shell.execute_reply":"2026-09-17T10:06:25.236616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def bootstrap_auc_ci(\n    y_true,\n    y_pred,\n    n_bootstrap=2000,\n    random_state=42\n):\n    \n    rng = np.random.default_rng(random_state)\n    n = len(y_true)\n    scores = []\n\n    for _ in range(n_bootstrap):\n\n        idx = rng.integers(0, n, n)\n\n        y_b = y_true[idx]\n        p_b = y_pred[idx]\n\n        if len(np.unique(y_b)) < 2:\n            continue\n\n        scores.append(\n            roc_auc_score(y_b, p_b)\n        )\n\n    scores = np.array(scores)\n\n    return (\n        roc_auc_score(y_true, y_pred),\n        np.percentile(scores, 2.5),\n        np.percentile(scores, 97.5)\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:06:25.239057Z","iopub.execute_input":"2026-09-17T10:06:25.239386Z","iopub.status.idle":"2026-09-17T10:06:25.246907Z","shell.execute_reply.started":"2026-09-17T10:06:25.239359Z","shell.execute_reply":"2026-09-17T10:06:25.245646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mri_ci_results = []\n\nfor i, target in enumerate(TARGET_COLS):\n\n    auc, lower, upper = bootstrap_auc_ci(\n        Y[:, i],\n        oof_predictions[:, i],\n        n_bootstrap=2000,\n        random_state=42\n    )\n\n    mri_ci_results.append({\n        \"Abnormality\": target,\n        \"AUC\": auc,\n        \"95% CI Lower\": lower,\n        \"95% CI Upper\": upper\n    })\n\nmri_ci_df = pd.DataFrame(mri_ci_results)\n\ndisplay(mri_ci_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:06:25.248736Z","iopub.execute_input":"2026-09-17T10:06:25.249251Z","iopub.status.idle":"2026-09-17T10:07:01.856771Z","shell.execute_reply.started":"2026-09-17T10:06:25.249211Z","shell.execute_reply":"2026-09-17T10:07:01.855633Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Interpretation of AUC with 95% Confidence Intervals\n\nThe ROC-AUC estimates show substantial variation in discrimination across the 12 abnormalities. Effusion had the highest AUC (0.801; 95% CI: 0.668–0.914), followed by lateral OA (0.789; 95% CI: 0.598–0.935) and lateral meniscus abnormalities (0.768; 95% CI: 0.639–0.883). These findings indicate relatively stronger discrimination for these abnormalities.\n\nModerate discrimination was observed for medial OA (AUC 0.726; 95% CI: 0.540–0.870), synovitis (0.711; 95% CI: 0.568–0.840), PF OA (0.695; 95% CI: 0.548–0.833), contusion (0.672; 95% CI: 0.521–0.815), and medial meniscus abnormalities (0.631; 95% CI: 0.479–0.774).\n\nPerformance was limited for ACL (0.555; 95% CI: 0.401–0.717), MCL (0.483; 95% CI: 0.260–0.705), and fracture (0.492; 95% CI: 0.333–0.644), with confidence intervals spanning or approaching 0.50. Baker's cyst showed the lowest AUC (0.386; 95% CI: 0.230–0.548","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import average_precision_score\n\npr_results = []\n\nfor i, target in enumerate(TARGET_COLS):\n\n    pr_auc = average_precision_score(\n        Y[:, i],\n        oof_predictions[:, i]\n    )\n\n    pr_results.append({\n        \"Abnormality\": target,\n        \"ROC_AUC\": mri_ci_df.loc[i, \"AUC\"],\n        \"PR_AUC\": pr_auc\n    })\n\npr_results_df = pd.DataFrame(pr_results)\n\ndisplay(pr_results_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:01.858316Z","iopub.execute_input":"2026-09-17T10:07:01.858786Z","iopub.status.idle":"2026-09-17T10:07:01.889318Z","shell.execute_reply.started":"2026-09-17T10:07:01.858742Z","shell.execute_reply":"2026-09-17T10:07:01.888042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model performance across knee abnormalities\n\nROC-AUC and PR-AUC for the 12 target abnormalities.\n\nEffusion showed the strongest discrimination (ROC-AUC = 0.801, PR-AUC = 0.857), indicating relatively good performance for identifying this abnormality.\nLateral OA (0.789) and Lateral Meniscus (0.768) also showed relatively strong ROC-AUC values.\nMedial OA (0.726), Synovitis (0.711), PF OA (0.695), and Contusion (0.672) demonstrated moderate discrimination.\nMedial Meniscus showed moderate performance, with ROC-AUC of 0.631 and PR-AUC of 0.663.\nACL (0.555), MCL (0.483), and Fracture (0.492) showed limited discrimination, with ROC-AUC values close to 0.50.\nBaker's cyst had the lowest performance (ROC-AUC = 0.386, PR-AUC = 0.178), suggesting poor discrimination in this dataset.\nPR-AUC was particularly low for MCL, Baker's cyst, and Fracture, which is important because PR-AUC reflects performance in identifying positive cases and is sensitive to class prevalence.","metadata":{}},{"cell_type":"code","source":"mean_pr_auc = pr_results_df[\"PR_AUC\"].mean()\n\nprint(\n    f\"Mean PR-AUC: {mean_pr_auc:.4f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:01.89079Z","iopub.execute_input":"2026-09-17T10:07:01.891231Z","iopub.status.idle":"2026-09-17T10:07:01.912055Z","shell.execute_reply.started":"2026-09-17T10:07:01.891188Z","shell.execute_reply":"2026-09-17T10:07:01.91097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nplt.figure(figsize=(10, 6))\n\nplt.bar(\n    mri_ci_df[\"Abnormality\"],\n    mri_ci_df[\"AUC\"]\n)\n\nplt.axhline(\n    0.5,\n    linestyle=\"--\"\n)\n\nplt.xticks(\n    rotation=45,\n    ha=\"right\"\n)\n\nplt.ylabel(\"ROC-AUC\")\nplt.xlabel(\"Abnormality\")\nplt.title(\"MRI-only ROC-AUC by Abnormality\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:01.913253Z","iopub.execute_input":"2026-09-17T10:07:01.913533Z","iopub.status.idle":"2026-09-17T10:07:02.171928Z","shell.execute_reply.started":"2026-09-17T10:07:01.91347Z","shell.execute_reply":"2026-09-17T10:07:02.171Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# Original pretrained ResNet\ncam_model = models.resnet18(\n    weights=models.ResNet18_Weights.DEFAULT\n)\n\ncam_model = cam_model.to(device)\ncam_model.eval()\n\n# Target convolutional layer\ntarget_layer = cam_model.layer4[-1]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.173389Z","iopub.execute_input":"2026-09-17T10:07:02.173776Z","iopub.status.idle":"2026-09-17T10:07:02.376545Z","shell.execute_reply.started":"2026-09-17T10:07:02.173736Z","shell.execute_reply":"2026-09-17T10:07:02.375434Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"activations = None\ngradients = None\n\ndef forward_hook(module, input, output):\n    global activations\n    activations = output\n\ndef backward_hook(module, grad_input, grad_output):\n    global gradients\n    gradients = grad_output[0]\n\nforward_handle = target_layer.register_forward_hook(\n    forward_hook\n)\n\nbackward_handle = target_layer.register_full_backward_hook(\n    backward_hook\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.378318Z","iopub.execute_input":"2026-09-17T10:07:02.379006Z","iopub.status.idle":"2026-09-17T10:07:02.38794Z","shell.execute_reply.started":"2026-09-17T10:07:02.378739Z","shell.execute_reply":"2026-09-17T10:07:02.386682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_gradcam(image_tensor, class_index=0):\n\n    global activations, gradients\n\n    image_tensor = image_tensor.to(device)\n    image_tensor.requires_grad = True\n\n    cam_model.zero_grad()\n\n    output = cam_model(image_tensor)\n\n    score = output[0, class_index]\n\n    score.backward()\n\n    # Global average pooling of gradients\n    weights = gradients.mean(\n        dim=(2, 3),\n        keepdim=True\n    )\n\n    cam = (\n        weights * activations\n    ).sum(dim=1, keepdim=True)\n\n    cam = F.relu(cam)\n\n    cam = F.interpolate(\n        cam,\n        size=image_tensor.shape[-2:],\n        mode=\"bilinear\",\n        align_corners=False\n    )\n\n    cam = cam.squeeze().detach().cpu().numpy()\n\n    # Normalize\n    cam -= cam.min()\n\n    if cam.max() > 0:\n        cam /= cam.max()\n\n    return cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.38925Z","iopub.execute_input":"2026-09-17T10:07:02.389598Z","iopub.status.idle":"2026-09-17T10:07:02.416795Z","shell.execute_reply.started":"2026-09-17T10:07:02.389559Z","shell.execute_reply":"2026-09-17T10:07:02.415623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_uid = labeled.iloc[0][\"StudyInstanceUID\"]\n\nseries_info = select_series(study_uid)\n\nseries_uid = series_info.iloc[0][\"SeriesInstanceUID\"]\n\nseries_dir = (\n    TRAIN_SERIES_DIR /\n    study_uid /\n    series_uid\n)\n\nvolume = load_series(series_dir)\n\nprint(\"Volume shape:\", volume.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.418338Z","iopub.execute_input":"2026-09-17T10:07:02.41902Z","iopub.status.idle":"2026-09-17T10:07:02.592549Z","shell.execute_reply.started":"2026-09-17T10:07:02.418919Z","shell.execute_reply":"2026-09-17T10:07:02.591381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"middle = volume.shape[0] // 2\n\nslice_img = volume[middle]\n\ntensor = transform(\n    (slice_img * 255).astype(np.uint8)\n).unsqueeze(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.594105Z","iopub.execute_input":"2026-09-17T10:07:02.594588Z","iopub.status.idle":"2026-09-17T10:07:02.603556Z","shell.execute_reply.started":"2026-09-17T10:07:02.594557Z","shell.execute_reply":"2026-09-17T10:07:02.602376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cam = generate_gradcam(\n    tensor,\n    class_index=0\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.604636Z","iopub.execute_input":"2026-09-17T10:07:02.604966Z","iopub.status.idle":"2026-09-17T10:07:02.808586Z","shell.execute_reply.started":"2026-09-17T10:07:02.604934Z","shell.execute_reply":"2026-09-17T10:07:02.807034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(7, 7))\n\nplt.imshow(slice_img, cmap=\"gray\")\nplt.imshow(cam, alpha=0.45)\n\nplt.title(\"Exploratory Grad-CAM\")\nplt.axis(\"off\")\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:02.809989Z","iopub.execute_input":"2026-09-17T10:07:02.810436Z","iopub.status.idle":"2026-09-17T10:07:03.131933Z","shell.execute_reply.started":"2026-09-17T10:07:02.810403Z","shell.execute_reply":"2026-09-17T10:07:03.130964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"error_df = labeled[[\"StudyInstanceUID\"] + TARGET_COLS].copy()\n\nfor i, target in enumerate(TARGET_COLS):\n    error_df[f\"{target}_Prediction\"] = oof_predictions[:, i]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.133232Z","iopub.execute_input":"2026-09-17T10:07:03.133627Z","iopub.status.idle":"2026-09-17T10:07:03.143142Z","shell.execute_reply.started":"2026-09-17T10:07:03.133564Z","shell.execute_reply":"2026-09-17T10:07:03.142098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target = \"ACL\"\n\nerror_df[\n    [\n        \"StudyInstanceUID\",\n        target,\n        f\"{target}_Prediction\"\n    ]\n].sort_values(\n    f\"{target}_Prediction\",\n    ascending=False\n).head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.14506Z","iopub.execute_input":"2026-09-17T10:07:03.145474Z","iopub.status.idle":"2026-09-17T10:07:03.177186Z","shell.execute_reply.started":"2026-09-17T10:07:03.145441Z","shell.execute_reply":"2026-09-17T10:07:03.17594Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"From a clinical perspective, the ACL model demonstrates good separation in predicted risk for some studies but also substantial false-positive predictions. Several studies with a true ACL label of 0 (negative) received very high predicted probabilities, such as 0.9993, 0.9961, 0.9941, and 0.9790. Conversely, several ACL-positive studies also received very high probabilities, including 0.9998, 0.9964, and 0.9810.\n\nThis pattern suggests that the model is confident but not consistently accurate in distinguishing ACL abnormalities from normal ACL findings. Clinically, a high predicted probability should therefore not be interpreted as confirmation of an ACL tear. The false-positive cases warrant review of the corresponding MRI images to determine whether the model is responding to related findings such as ligament signal alteration, partial injury, postoperative change, imaging artifacts, or other peri-ligamentous abnormalities.\n\nGiven the ACL ROC-AUC of 0.555, the model currently provides limited discrimination for ACL abnormality in this small validation dataset. These predictions should therefore be regarded as decision-support signals rather than diagnostic conclusions, and further validation with a larger independently labeled cohort is required.","metadata":{}},{"cell_type":"code","source":"target = \"ACL\"\n\nerror_df[\"Predicted\"] = (\n    error_df[f\"{target}_Prediction\"] >= 0.5\n).astype(int)\n\nfalse_positive = error_df[\n    (error_df[target] == 0) &\n    (error_df[\"Predicted\"] == 1)\n]\n\nfalse_negative = error_df[\n    (error_df[target] == 1) &\n    (error_df[\"Predicted\"] == 0)\n]\n\nprint(\"False positives:\", len(false_positive))\nprint(\"False negatives:\", len(false_negative))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.178321Z","iopub.execute_input":"2026-09-17T10:07:03.178963Z","iopub.status.idle":"2026-09-17T10:07:03.206039Z","shell.execute_reply.started":"2026-09-17T10:07:03.178919Z","shell.execute_reply":"2026-09-17T10:07:03.204922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"error_summary = []\n\nfor target in TARGET_COLS:\n\n    pred = (\n        error_df[f\"{target}_Prediction\"] >= 0.5\n    ).astype(int)\n\n    fp = (\n        (error_df[target] == 0) &\n        (pred == 1)\n    ).sum()\n\n    fn = (\n        (error_df[target] == 1) &\n        (pred == 0)\n    ).sum()\n\n    error_summary.append({\n        \"Abnormality\": target,\n        \"False_Positive\": fp,\n        \"False_Negative\": fn\n    })\n\nerror_summary_df = pd.DataFrame(error_summary)\n\ndisplay(error_summary_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.207183Z","iopub.execute_input":"2026-09-17T10:07:03.207544Z","iopub.status.idle":"2026-09-17T10:07:03.247488Z","shell.execute_reply.started":"2026-09-17T10:07:03.207501Z","shell.execute_reply":"2026-09-17T10:07:03.246105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"final_comparison = pd.DataFrame({\n    \"Abnormality\": TARGET_COLS,\n    \"MRI_AUC\": results_df[\"MRI_AUC\"].values,\n    \"Text_AUC\": text_results_df[\"Text_AUC\"].values,\n    \"Multimodal_AUC\": multimodal_results_df[\"Multimodal_AUC\"].values\n})\n\ndisplay(final_comparison)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.249184Z","iopub.execute_input":"2026-09-17T10:07:03.249985Z","iopub.status.idle":"2026-09-17T10:07:03.287658Z","shell.execute_reply.started":"2026-09-17T10:07:03.249929Z","shell.execute_reply":"2026-09-17T10:07:03.286284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# MACRO ROC-AUC\n# ============================================================\n\nfrom sklearn.metrics import roc_auc_score\nimport numpy as np\nimport pandas as pd\n\ndef calculate_macro_auc(y_true, y_pred, target_cols):\n    \"\"\"\n    Calculate ROC-AUC for each abnormality and\n    macro-average ROC-AUC across all 12 abnormalities.\n    \"\"\"\n\n    auc_results = {}\n\n    for i, target in enumerate(target_cols):\n\n        # Check that both classes are present\n        if len(np.unique(y_true[:, i])) < 2:\n            auc_results[target] = np.nan\n        else:\n            auc_results[target] = roc_auc_score(\n                y_true[:, i],\n                y_pred[:, i]\n            )\n\n    # Macro-average across valid abnormalities\n    macro_auc = np.nanmean(list(auc_results.values()))\n\n    return auc_results, macro_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.289385Z","iopub.execute_input":"2026-09-17T10:07:03.290102Z","iopub.status.idle":"2026-09-17T10:07:03.319485Z","shell.execute_reply.started":"2026-09-17T10:07:03.290058Z","shell.execute_reply":"2026-09-17T10:07:03.318138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# MRI MODEL PERFORMANCE\n# ============================================================\n\nauc_results, macro_auc = calculate_macro_auc(\n    Y,\n    oof_predictions,\n    TARGET_COLS\n)\n\nprint(\"=\" * 50)\nprint(\"MRI MODEL PERFORMANCE\")\nprint(\"=\" * 50)\n\nfor target, auc in auc_results.items():\n    print(f\"{target:20s}: {auc:.4f}\")\n\nprint(\"-\" * 50)\nprint(f\"Macro ROC-AUC: {macro_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.32105Z","iopub.execute_input":"2026-09-17T10:07:03.322059Z","iopub.status.idle":"2026-09-17T10:07:03.386073Z","shell.execute_reply.started":"2026-09-17T10:07:03.322017Z","shell.execute_reply":"2026-09-17T10:07:03.384913Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"auc_table = pd.DataFrame({\n    \"Abnormality\": list(auc_results.keys()),\n    \"ROC-AUC\": list(auc_results.values())\n})\n\nauc_table","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.387627Z","iopub.execute_input":"2026-09-17T10:07:03.387958Z","iopub.status.idle":"2026-09-17T10:07:03.425133Z","shell.execute_reply.started":"2026-09-17T10:07:03.387931Z","shell.execute_reply":"2026-09-17T10:07:03.424001Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"individual_auc = []\n\nfor i, target in enumerate(TARGET_COLS):\n\n    auc = roc_auc_score(\n        Y[:, i],\n        oof_predictions[:, i]\n    )\n\n    individual_auc.append(auc)\n\nmacro_auc = sum(individual_auc) / len(individual_auc)\n\nprint(\"Individual AUCs:\")\nprint(individual_auc)\n\nprint(\"\\nMacro ROC-AUC:\")\nprint(f\"{macro_auc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.426651Z","iopub.execute_input":"2026-09-17T10:07:03.427101Z","iopub.status.idle":"2026-09-17T10:07:03.480358Z","shell.execute_reply.started":"2026-09-17T10:07:03.427054Z","shell.execute_reply":"2026-09-17T10:07:03.479214Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The comparison showed that model performance varied across the 12 knee abnormalities. The MRI-only model achieved a macro-average ROC-AUC of 0.6424, indicating moderate overall discriminative performance, with the highest AUCs observed for effusion (0.801), lateral OA (0.789), and lateral meniscus abnormalities (0.768). The text-only model achieved a macro-AUC of 0.5589, showing lower overall discrimination, although it performed relatively well for medial OA (0.797) and medial meniscus abnormalities (0.709). The simple multimodal model achieved a macro-AUC of 0.6435, which was very similar to the MRI-only model, suggesting that a straightforward combination of MRI and report features provided little additional discrimination in this small dataset. Overall, the findings support the potential value of MRI-based prediction while highlighting substantial variation between abnormalities and the need for validation using a larger independently labeled dataset.","metadata":{}},{"cell_type":"code","source":"TARGET_COLS = [\n    \"ACL\",\n    \"MCL\",\n    \"Medial Meniscus\",\n    \"Lateral Meniscus\",\n    \"Medial OA\",\n    \"Lateral OA\",\n    \"PF OA\",\n    \"Effusion\",\n    \"Synovitis\",\n    \"Baker's\",\n    \"Contusion\",\n    \"Fracture\"\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.4816Z","iopub.execute_input":"2026-09-17T10:07:03.482401Z","iopub.status.idle":"2026-09-17T10:07:03.505269Z","shell.execute_reply.started":"2026-09-17T10:07:03.482369Z","shell.execute_reply":"2026-09-17T10:07:03.504027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score\nimport numpy as np\n\ndef competition_score(y_true, y_pred):\n    \"\"\"\n    Competition metric:\n    Macro-averaged ROC-AUC across 12 targets.\n    \"\"\"\n\n    aucs = []\n\n    for i, target in enumerate(TARGET_COLS):\n\n        # ROC-AUC requires both positive and negative observations\n        if len(np.unique(y_true[:, i])) < 2:\n            continue\n\n        auc = roc_auc_score(\n            y_true[:, i],\n            y_pred[:, i]\n        )\n\n        aucs.append(auc)\n\n    return np.mean(aucs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.50674Z","iopub.execute_input":"2026-09-17T10:07:03.507137Z","iopub.status.idle":"2026-09-17T10:07:03.533569Z","shell.execute_reply.started":"2026-09-17T10:07:03.507099Z","shell.execute_reply":"2026-09-17T10:07:03.532205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"score = competition_score(\n    Y,\n    oof_predictions\n)\n\nprint(f\"Macro ROC-AUC: {score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.535182Z","iopub.execute_input":"2026-09-17T10:07:03.535672Z","iopub.status.idle":"2026-09-17T10:07:03.596341Z","shell.execute_reply.started":"2026-09-17T10:07:03.53564Z","shell.execute_reply":"2026-09-17T10:07:03.595328Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"score = competition_score(\n    Y,\n    oof_predictions\n)\n\nprint(f\"Macro ROC-AUC: {score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.597662Z","iopub.execute_input":"2026-09-17T10:07:03.598052Z","iopub.status.idle":"2026-09-17T10:07:03.651364Z","shell.execute_reply.started":"2026-09-17T10:07:03.598013Z","shell.execute_reply":"2026-09-17T10:07:03.65023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"score = competition_score(\n    Y,\n    oof_predictions\n)\n\nprint(f\"Macro ROC-AUC: {score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:07:03.652857Z","iopub.execute_input":"2026-09-17T10:07:03.653694Z","iopub.status.idle":"2026-09-17T10:07:03.705822Z","shell.execute_reply.started":"2026-09-17T10:07:03.653658Z","shell.execute_reply":"2026-09-17T10:07:03.704643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CREATE FINAL KAGGLE SUBMISSION FILE\n# ============================================================\n\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\n\n# ------------------------------------------------------------\n# 1. Kaggle paths\n# ------------------------------------------------------------\n\nDATA_DIR = Path(\n    \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n)\n\nSAMPLE_SUBMISSION = DATA_DIR / \"sample_submission.csv\"\n\nOUTPUT_FILE = \"/kaggle/working/submission.csv\"\n\n\n# ------------------------------------------------------------\n# 2. Target columns\n# ------------------------------------------------------------\n\nTARGET_COLS = [\n    \"ACL\",\n    \"MCL\",\n    \"Medial Meniscus\",\n    \"Lateral Meniscus\",\n    \"Medial OA\",\n    \"Lateral OA\",\n    \"PF OA\",\n    \"Effusion\",\n    \"Synovitis\",\n    \"Baker's\",\n    \"Contusion\",\n    \"Fracture\"\n]\n\n\n# ------------------------------------------------------------\n# 3. Load official submission template\n# ------------------------------------------------------------\n\nsample_submission = pd.read_csv(\n    SAMPLE_SUBMISSION\n)\n\nprint(\"Expected submission shape:\")\nprint(sample_submission.shape)\n\nprint(\"\\nExpected columns:\")\nprint(sample_submission.columns.tolist())\n\n\n# ------------------------------------------------------------\n# 4. Check that test predictions exist\n# ------------------------------------------------------------\n\nif \"test_predictions\" not in globals():\n    raise NameError(\n        \"test_predictions does not exist. \"\n        \"Run the final model prediction cell first.\"\n    )\n\nprint(\"\\nPrediction array shape:\")\nprint(test_predictions.shape)\n\n\n# ------------------------------------------------------------\n# 5. Check prediction dimensions\n# ------------------------------------------------------------\n\nif test_predictions.shape[1] != len(TARGET_COLS):\n    raise ValueError(\n        f\"Expected {len(TARGET_COLS)} prediction columns, \"\n        f\"but received {test_predictions.shape[1]}.\"\n    )\n\nif test_predictions.shape[0] != len(sample_submission):\n    raise ValueError(\n        f\"Number of predictions ({test_predictions.shape[0]}) \"\n        f\"does not match test studies \"\n        f\"({len(sample_submission)}).\"\n    )\n\n\n# ------------------------------------------------------------\n# 6. Validate probability predictions\n# ------------------------------------------------------------\n\nif np.isnan(test_predictions).any():\n    raise ValueError(\"Predictions contain NaN values.\")\n\nif np.isinf(test_predictions).any():\n    raise ValueError(\"Predictions contain infinite values.\")\n\nif (test_predictions < 0).any() or (test_predictions > 1).any():\n    raise ValueError(\n        \"Predictions must be between 0 and 1.\"\n    )\n\n\n# ------------------------------------------------------------\n# 7. Create submission\n# ------------------------------------------------------------\n\nsubmission = sample_submission.copy()\n\nfor i, target in enumerate(TARGET_COLS):\n\n    submission[target] = test_predictions[:, i]\n\n\n# ------------------------------------------------------------\n# 8. Ensure exact column order\n# ------------------------------------------------------------\n\nsubmission = submission[\n    [\"StudyInstanceUID\"] + TARGET_COLS\n]\n\n\n# ------------------------------------------------------------\n# 9. Final validation\n# ------------------------------------------------------------\n\nassert len(submission) == len(sample_submission)\n\nassert submission.columns.tolist() == (\n    [\"StudyInstanceUID\"] + TARGET_COLS\n)\n\nassert submission[TARGET_COLS].notna().all().all()\n\nassert (\n    (submission[TARGET_COLS] >= 0) &\n    (submission[TARGET_COLS] <= 1)\n).all().all()\n\n\n# ------------------------------------------------------------\n# 10. Save submission.csv\n# ------------------------------------------------------------\n\nsubmission.to_csv(\n    OUTPUT_FILE,\n    index=False\n)\n\n\n# ------------------------------------------------------------\n# 11. Display result\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"SUBMISSION CREATED SUCCESSFULLY\")\nprint(\"=\" * 60)\n\nprint(f\"File: {OUTPUT_FILE}\")\nprint(f\"Rows: {len(submission)}\")\nprint(f\"Columns: {len(submission.columns)}\")\n\nprint(\"\\nFirst 5 rows:\")\ndisplay(submission.head())\n\nprint(\"\\nPrediction summary:\")\ndisplay(\n    submission[TARGET_COLS].describe().T\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:26:12.481206Z","iopub.execute_input":"2026-09-17T10:26:12.482129Z","iopub.status.idle":"2026-09-17T10:26:12.505176Z","shell.execute_reply.started":"2026-09-17T10:26:12.482094Z","shell.execute_reply":"2026-09-17T10:26:12.503225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"X shape:\", X.shape)\nprint(\"Y shape:\", Y.shape)\nprint(\"X_test shape:\", X_test.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:27:26.25017Z","iopub.execute_input":"2026-09-17T10:27:26.251525Z","iopub.status.idle":"2026-09-17T10:27:26.261661Z","shell.execute_reply.started":"2026-09-17T10:27:26.251462Z","shell.execute_reply":"2026-09-17T10:27:26.260059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CREATE X_TEST FROM TEST MRI STUDIES\n# ============================================================\n\nfrom pathlib import Path\nimport numpy as np\nimport pydicom\nimport torch\n\nDATA_DIR = Path(\n    \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n)\n\nTEST_SERIES_DIR = DATA_DIR / \"test_series\"\n\n\n# ------------------------------------------------------------\n# Select one series for each study\n# ------------------------------------------------------------\n\ndef select_test_series(study_uid):\n\n    df = test_series[\n        test_series[\"StudyInstanceUID\"] == study_uid\n    ]\n\n    if len(df) == 0:\n        return None\n\n    # Prefer fluid-sensitive series\n    if \"Fluid_Sensitive\" in df.columns:\n\n        fluid = df[df[\"Fluid_Sensitive\"] == 1]\n\n        if len(fluid) > 0:\n            return fluid.iloc[0][\"SeriesInstanceUID\"]\n\n    # Otherwise use first series\n    return df.iloc[0][\"SeriesInstanceUID\"]\n\n\n# ------------------------------------------------------------\n# Load DICOM series\n# ------------------------------------------------------------\n\ndef load_test_volume(series_path):\n\n    dicom_files = list(series_path.glob(\"*.dcm\"))\n\n    if len(dicom_files) == 0:\n        return None\n\n    slices = []\n\n    for file in dicom_files:\n\n        try:\n            ds = pydicom.dcmread(file)\n\n            image = ds.pixel_array.astype(np.float32)\n\n            # DICOM intensity correction\n            slope = float(\n                getattr(ds, \"RescaleSlope\", 1.0)\n            )\n\n            intercept = float(\n                getattr(ds, \"RescaleIntercept\", 0.0)\n            )\n\n            image = image * slope + intercept\n\n            # Spatial position\n            if hasattr(ds, \"ImagePositionPatient\"):\n                position = float(\n                    ds.ImagePositionPatient[2]\n                )\n            else:\n                position = float(\n                    getattr(ds, \"InstanceNumber\", 0)\n                )\n\n            slices.append(\n                (position, image)\n            )\n\n        except Exception:\n            continue\n\n    if len(slices) == 0:\n        return None\n\n    # Sort slices\n    slices.sort(key=lambda x: x[0])\n\n    volume = np.stack(\n        [x[1] for x in slices],\n        axis=0\n    )\n\n    # Robust normalization\n    p1 = np.percentile(volume, 1)\n    p99 = np.percentile(volume, 99)\n\n    volume = np.clip(\n        volume,\n        p1,\n        p99\n    )\n\n    if p99 > p1:\n        volume = (\n            volume - p1\n        ) / (\n            p99 - p1\n        )\n\n    return volume.astype(np.float32)\n\n\n# ------------------------------------------------------------\n# Extract ResNet features\n# ------------------------------------------------------------\n\ndef extract_test_features(volume):\n\n    # Select 16 evenly spaced slices\n    n_slices = min(16, volume.shape[0])\n\n    indices = np.linspace(\n        0,\n        volume.shape[0] - 1,\n        n_slices\n    ).astype(int)\n\n    slices = volume[indices]\n\n    images = []\n\n    for image in slices:\n\n        image_tensor = transform(image)\n        images.append(image_tensor)\n\n    batch = torch.stack(images).to(device)\n\n    with torch.no_grad():\n\n        features = feature_extractor(batch)\n\n    # [N, 512, 1, 1]\n    features = features.squeeze(-1).squeeze(-1)\n\n    # Study-level feature\n    study_feature = features.mean(dim=0)\n\n    return study_feature.cpu().numpy()\n\n\n# ============================================================\n# PROCESS TEST DATA\n# ============================================================\n\nX_test_list = []\ntest_uids = []\nfailed_studies = []\n\ntotal = len(test)\n\nprint(\"=\" * 60)\nprint(\"EXTRACTING TEST MRI FEATURES\")\nprint(\"=\" * 60)\n\nfor i, study_uid in enumerate(\n    test[\"StudyInstanceUID\"]\n):\n\n    try:\n\n        # Find series\n        series_uid = select_test_series(\n            study_uid\n        )\n\n        if series_uid is None:\n            failed_studies.append(study_uid)\n            continue\n\n        # Series path\n        series_path = (\n            TEST_SERIES_DIR\n            / study_uid\n            / series_uid\n        )\n\n        if not series_path.exists():\n            failed_studies.append(study_uid)\n            continue\n\n        # Load MRI\n        volume = load_test_volume(\n            series_path\n        )\n\n        if volume is None:\n            failed_studies.append(study_uid)\n            continue\n\n        # Extract features\n        features = extract_test_features(\n            volume\n        )\n\n        X_test_list.append(features)\n        test_uids.append(study_uid)\n\n        if (i + 1) % 25 == 0:\n\n            print(\n                f\"Processed {i + 1}/{total} \"\n                f\"| Successful: {len(test_uids)} \"\n                f\"| Failed: {len(failed_studies)}\"\n            )\n\n    except Exception as e:\n\n        failed_studies.append(study_uid)\n\n        print(\n            f\"Error in study {study_uid}: {e}\"\n        )\n\n\n# ============================================================\n# CREATE X_TEST\n# ============================================================\n\nX_test = np.asarray(\n    X_test_list,\n    dtype=np.float32\n)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"TEST FEATURE EXTRACTION COMPLETE\")\nprint(\"=\" * 60)\n\nprint(\"Training X shape :\", X.shape)\nprint(\"Training Y shape :\", Y.shape)\nprint(\"Test X_test shape:\", X_test.shape)\nprint(\"Successful       :\", len(test_uids))\nprint(\"Failed           :\", len(failed_studies))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:28:18.8502Z","iopub.execute_input":"2026-09-17T10:28:18.851133Z","iopub.status.idle":"2026-09-17T10:28:23.531452Z","shell.execute_reply.started":"2026-09-17T10:28:18.851084Z","shell.execute_reply":"2026-09-17T10:28:23.530758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK TEST DATA\n# ============================================================\n\nprint(\"Number of rows in test.csv:\", len(test))\nprint(\"Number of unique studies:\", test[\"StudyInstanceUID\"].nunique())\n\nprint(\"\\nFirst 10 test studies:\")\ndisplay(test[[\"StudyInstanceUID\"]].head(10))\n\nprint(\"\\nTest series shape:\", test_series.shape)\n\nprint(\"\\nTest series per study:\")\nprint(\n    test_series[\"StudyInstanceUID\"]\n    .nunique()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:29:16.889501Z","iopub.execute_input":"2026-09-17T10:29:16.88995Z","iopub.status.idle":"2026-09-17T10:29:16.910112Z","shell.execute_reply.started":"2026-09-17T10:29:16.88991Z","shell.execute_reply":"2026-09-17T10:29:16.909178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK KAGGLE DIRECTORY\n# ============================================================\n\nprint(\"DATA_DIR:\")\nprint(DATA_DIR)\n\nprint(\"\\nFiles/folders:\")\n\nfor item in DATA_DIR.iterdir():\n    print(item.name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:29:32.727696Z","iopub.execute_input":"2026-09-17T10:29:32.728099Z","iopub.status.idle":"2026-09-17T10:29:32.73728Z","shell.execute_reply.started":"2026-09-17T10:29:32.728067Z","shell.execute_reply":"2026-09-17T10:29:32.736183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(test.shape)\nprint(test.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:29:44.343472Z","iopub.execute_input":"2026-09-17T10:29:44.343914Z","iopub.status.idle":"2026-09-17T10:29:44.350483Z","shell.execute_reply.started":"2026-09-17T10:29:44.343857Z","shell.execute_reply":"2026-09-17T10:29:44.349389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK COMPLETE KAGGLE DATASET\n# ============================================================\n\nfrom pathlib import Path\n\nDATA_DIR = Path(\n    \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n)\n\nprint(\"=\" * 60)\nprint(\"KAGGLE DATASET CONTENT\")\nprint(\"=\" * 60)\n\nfor path in sorted(DATA_DIR.iterdir()):\n    print(path.name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:30:45.06187Z","iopub.execute_input":"2026-09-17T10:30:45.062279Z","iopub.status.idle":"2026-09-17T10:30:45.071011Z","shell.execute_reply.started":"2026-09-17T10:30:45.062251Z","shell.execute_reply":"2026-09-17T10:30:45.069957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK CSV FILES\n# ============================================================\n\nimport pandas as pd\n\nfor csv_file in DATA_DIR.glob(\"*.csv\"):\n\n    df = pd.read_csv(csv_file)\n\n    print(\n        f\"{csv_file.name:25s} \"\n        f\"shape = {df.shape}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:30:52.24602Z","iopub.execute_input":"2026-09-17T10:30:52.246363Z","iopub.status.idle":"2026-09-17T10:30:52.447955Z","shell.execute_reply.started":"2026-09-17T10:30:52.246336Z","shell.execute_reply":"2026-09-17T10:30:52.44675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK SAMPLE SUBMISSION\n# ============================================================\n\nsample_submission = pd.read_csv(\n    DATA_DIR / \"sample_submission.csv\"\n)\n\nprint(\"Sample submission shape:\")\nprint(sample_submission.shape)\n\nprint(\"\\nColumns:\")\nprint(sample_submission.columns.tolist())\n\nprint(\"\\nFirst rows:\")\ndisplay(sample_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:31:02.991318Z","iopub.execute_input":"2026-09-17T10:31:02.992251Z","iopub.status.idle":"2026-09-17T10:31:03.015491Z","shell.execute_reply.started":"2026-09-17T10:31:02.992217Z","shell.execute_reply":"2026-09-17T10:31:03.014609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# COMPARE TEST AND SAMPLE SUBMISSION\n# ============================================================\n\nprint(\"Test studies:\",\n      test[\"StudyInstanceUID\"].nunique())\n\nprint(\"Sample submission studies:\",\n      sample_submission[\"StudyInstanceUID\"].nunique())\n\nprint(\n    \"UIDs match:\",\n    set(test[\"StudyInstanceUID\"])\n    == set(sample_submission[\"StudyInstanceUID\"])\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:31:10.574835Z","iopub.execute_input":"2026-09-17T10:31:10.575956Z","iopub.status.idle":"2026-09-17T10:31:10.583398Z","shell.execute_reply.started":"2026-09-17T10:31:10.575826Z","shell.execute_reply":"2026-09-17T10:31:10.582281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FINAL TEST PREDICTIONS\n# ============================================================\n\nfrom sklearn.linear_model import LogisticRegression\nimport numpy as np\n\nTARGET_COLS = [\n    \"ACL\",\n    \"MCL\",\n    \"Medial Meniscus\",\n    \"Lateral Meniscus\",\n    \"Medial OA\",\n    \"Lateral OA\",\n    \"PF OA\",\n    \"Effusion\",\n    \"Synovitis\",\n    \"Baker's\",\n    \"Contusion\",\n    \"Fracture\"\n]\n\ntest_predictions = np.zeros(\n    (len(X_test), len(TARGET_COLS)),\n    dtype=np.float32\n)\n\nfor i, target in enumerate(TARGET_COLS):\n\n    print(f\"Training: {target}\")\n\n    model = LogisticRegression(\n        max_iter=2000,\n        solver=\"liblinear\",\n        random_state=42\n    )\n\n    model.fit(\n        X,\n        Y[:, i]\n    )\n\n    # Continuous probability\n    test_predictions[:, i] = (\n        model.predict_proba(X_test)[:, 1]\n    )\n\nprint(\"\\nPrediction shape:\")\nprint(test_predictions.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:31:41.054652Z","iopub.execute_input":"2026-09-17T10:31:41.055729Z","iopub.status.idle":"2026-09-17T10:31:41.149351Z","shell.execute_reply.started":"2026-09-17T10:31:41.055692Z","shell.execute_reply":"2026-09-17T10:31:41.14831Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CREATE SUBMISSION\n# ============================================================\n\nsubmission = sample_submission.copy()\n\nsubmission[TARGET_COLS] = test_predictions\n\n# Make sure the exact required column order is retained\nsubmission = submission[\n    [\"StudyInstanceUID\"] + TARGET_COLS\n]\n\nprint(\"Submission:\")\ndisplay(submission)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:31:51.582717Z","iopub.execute_input":"2026-09-17T10:31:51.583733Z","iopub.status.idle":"2026-09-17T10:31:51.604702Z","shell.execute_reply.started":"2026-09-17T10:31:51.583698Z","shell.execute_reply":"2026-09-17T10:31:51.603587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# VALIDATE SUBMISSION\n# ============================================================\n\nassert submission.shape == sample_submission.shape\n\nassert submission.columns.tolist() == (\n    [\"StudyInstanceUID\"] + TARGET_COLS\n)\n\nassert submission[TARGET_COLS].notna().all().all()\n\nassert (\n    (submission[TARGET_COLS] >= 0) &\n    (submission[TARGET_COLS] <= 1)\n).all().all()\n\nprint(\"✓ Submission validation passed\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:32:07.509586Z","iopub.execute_input":"2026-09-17T10:32:07.510487Z","iopub.status.idle":"2026-09-17T10:32:07.529279Z","shell.execute_reply.started":"2026-09-17T10:32:07.510449Z","shell.execute_reply":"2026-09-17T10:32:07.528149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# SAVE SUBMISSION.CSV\n# ============================================================\n\nsubmission.to_csv(\n    \"/kaggle/working/submission.csv\",\n    index=False\n)\n\nprint(\"✓ submission.csv created\")\nprint(\"/kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:40:51.568624Z","iopub.execute_input":"2026-09-17T10:40:51.57Z","iopub.status.idle":"2026-09-17T10:40:51.580723Z","shell.execute_reply.started":"2026-09-17T10:40:51.569953Z","shell.execute_reply":"2026-09-17T10:40:51.579654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.to_csv(\n    \"/kaggle/working/submission.csv\",\n    index=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:42:58.943314Z","iopub.execute_input":"2026-09-17T10:42:58.943753Z","iopub.status.idle":"2026-09-17T10:42:58.951049Z","shell.execute_reply.started":"2026-09-17T10:42:58.943706Z","shell.execute_reply":"2026-09-17T10:42:58.949848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nsubmission = pd.read_csv(\"/kaggle/working/submission.csv\")\n\nprint(submission.shape)\nprint(submission.head())\nprint(submission.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:44:02.414984Z","iopub.execute_input":"2026-09-17T10:44:02.416107Z","iopub.status.idle":"2026-09-17T10:44:02.441516Z","shell.execute_reply.started":"2026-09-17T10:44:02.416068Z","shell.execute_reply":"2026-09-17T10:44:02.440225Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nsample_submission = pd.read_csv(\n    \"/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv\"\n)\n\nsubmission = pd.read_csv(\"/kaggle/working/submission.csv\")\n\nprint(\"Sample submission:\", sample_submission.shape)\nprint(\"Our submission:\", submission.shape)\n\nprint(\"UIDs match:\",\n      submission[\"StudyInstanceUID\"].equals(\n          sample_submission[\"StudyInstanceUID\"]\n      ))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:44:37.965608Z","iopub.execute_input":"2026-09-17T10:44:37.966025Z","iopub.status.idle":"2026-09-17T10:44:37.980378Z","shell.execute_reply.started":"2026-09-17T10:44:37.965993Z","shell.execute_reply":"2026-09-17T10:44:37.978988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nDATA_DIR = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\nfor name in [\"train.csv\", \"test.csv\", \"train_series.csv\",\n             \"test_series.csv\", \"sample_submission.csv\"]:\n\n    df = pd.read_csv(f\"{DATA_DIR}/{name}\")\n\n    print(f\"{name:25s} {df.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T09:41:02.430904Z","iopub.execute_input":"2026-09-19T09:41:02.431533Z","iopub.status.idle":"2026-09-19T09:41:02.610886Z","shell.execute_reply.started":"2026-09-19T09:41:02.431502Z","shell.execute_reply":"2026-09-19T09:41:02.610113Z"}},"outputs":[],"execution_count":null}]}