{"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":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 3 — IMPROVED TRAINING\n# ============================================\n\nimport torch\nimport numpy as np\n\nnum_epochs = 10\nbest_val_loss = float(\"inf\")\n\nprint(\"Starting improved training...\")\nprint(\"=\" * 60)\n\nfor epoch in range(num_epochs):\n\n    # =========================\n    # TRAINING\n    # =========================\n    model.train()\n\n    train_loss = 0.0\n    train_batches = 0\n\n    for images, labels in train_loader:\n\n        images = images.to(device)\n        labels = labels.to(device).float()\n\n        optimizer.zero_grad()\n\n        outputs = model(images)\n\n        loss = criterion(outputs, labels)\n\n        loss .backward()\n\n        optimizer.step()\n\n        train_loss += loss.item()\n        train_batches += 1\n\n    train_loss = train_loss / max(train_batches, 1)\n\n    # =========================\n    # VALIDATION\n    # =========================\n    model.eval()\n\n    val_loss = 0.0\n    val_batches = 0\n\n    all_predictions = []\n    all_true_labels = []\n\n    with torch.no_grad():\n\n        for images, labels in val_loader:\n\n            images = images.to(device)\n            labels = labels.to(device).float()\n\n            outputs = model(images)\n\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item()\n            val_batches += 1\n\n            probabilities = torch.sigmoid(outputs)\n\n            all_predictions.append(\n                probabilities.cpu().numpy()\n            )\n\n            all_true_labels.append(\n                labels.cpu().numpy()\n            )\n\n    val_loss = val_loss / max(val_batches, 1)\n\n    all_predictions = np.concatenate(all_predictions)\n    all_true_labels = np.concatenate(all_true_labels)\n\n    # =========================\n    # PRINT RESULTS\n    # =========================\n\n    print(\n        f\"Epoch {epoch + 1}/{num_epochs} | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Val Loss: {val_loss:.4f}\"\n    )\n\n    # =========================\n    # SAVE BEST MODEL\n    # =========================\n\n    if val_loss < best_val_loss:\n\n        best_val_loss = val_loss\n\n        torch.save(\n            model.state_dict(),\n            \"/kaggle/working/rsna_knee_resnet18_improved.pth\"\n        )\n\n        print(\"✓ Best model saved!\")\n\nprint(\"=\" * 60)\nprint(\"Training completed!\")\nprint(f\"Best validation loss: {best_val_loss:.4f}\")\nprint()\nprint(\"Model saved at:\")\nprint(\"/kaggle/working/rsna_knee_resnet18_improved.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:45:47.748018Z","iopub.execute_input":"2026-08-08T08:45:47.748441Z","iopub.status.idle":"2026-08-08T08:45:51.415537Z","shell.execute_reply.started":"2026-08-08T08:45:47.748406Z","shell.execute_reply":"2026-08-08T08:45:51.414337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 1 — IMPORTS AND SETUP\n# ============================================\n\nimport os\nimport glob\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\n\nimport pydicom\nfrom PIL import Image\n\nimport torchvision\nfrom torchvision import models, transforms\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import (\n    roc_auc_score,\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score\n)\n\n# Device\ndevice = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"PyTorch:\", torch.__version__)\nprint(\"Torchvision:\", torchvision.__version__)\nprint(\"Device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:46:43.312983Z","iopub.execute_input":"2026-08-08T08:46:43.313704Z","iopub.status.idle":"2026-08-08T08:46:49.370945Z","shell.execute_reply.started":"2026-08-08T08:46:43.313666Z","shell.execute_reply":"2026-08-08T08:46:49.369803Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 2 — FIND DATA AND LOAD CSV FILES\n# ============================================\n\nimport os\nimport pandas as pd\n\n# Search for the project folder\nbase_candidates = [\n    \"/kaggle/working/rsna-knee-abnormality-detection\",\n    \"/kaggle/input/rsna-knee-abnormality-detection\",\n    \"/kaggle/working\"\n]\n\nbase_dir = None\n\nfor path in base_candidates:\n    if os.path.exists(path):\n        base_dir = path\n        print(\"Using folder:\", path)\n        break\n\nif base_dir is None:\n    raise FileNotFoundError(\"RSNA project folder not found.\")\n\nprint(\"\\nFiles/Folders:\")\nprint(os.listdir(base_dir)[:30])\n\n# CSV paths\ntrain_csv_path = os.path.join(base_dir, \"train.csv\")\ntest_csv_path = os.path.join(base_dir, \"test.csv\")\ntrain_series_csv_path = os.path.join(base_dir, \"train_series.csv\")\ntest_series_csv_path = os.path.join(base_dir, \"test_series.csv\")\n\n# Check files\nprint(\"\\nCSV file check:\")\n\nfor path in [\n    train_csv_path,\n    test_csv_path,\n    train_series_csv_path,\n    test_series_csv_path\n]:\n    print(os.path.basename(path), \"->\", os.path.exists(path))\n\n# Load CSVs\ntrain_df = pd.read_csv(train_csv_path)\ntest_df = pd.read_csv(test_csv_path)\ntrain_series_df = pd.read_csv(train_series_csv_path)\ntest_series_df = pd.read_csv(test_series_csv_path)\n\nprint(\"\\nTrain CSV shape:\", train_df.shape)\nprint(\"Test CSV shape:\", test_df.shape)\nprint(\"Train series shape:\", train_series_df.shape)\nprint(\"Test series shape:\", test_series_df.shape)\n\nprint(\"\\nTrain columns:\")\nprint(train_df.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:47:33.351508Z","iopub.execute_input":"2026-08-08T08:47:33.352006Z","iopub.status.idle":"2026-08-08T08:47:33.376847Z","shell.execute_reply.started":"2026-08-08T08:47:33.351974Z","shell.execute_reply":"2026-08-08T08:47:33.375561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# FIND RSNA DATASET LOCATION\n# ============================================\n\nimport os\n\nprint(\"Kaggle input folders:\")\nif os.path.exists(\"/kaggle/input\"):\n    print(os.listdir(\"/kaggle/input\"))\n\nprint(\"\\nKaggle working folders:\")\nprint(os.listdir(\"/kaggle/working\"))\n\nprint(\"\\nSearching for train.csv ...\")\n\nfound = []\n\nfor root, dirs, files in os.walk(\"/kaggle\"):\n    if \"train.csv\" in files:\n        found.append(os.path.join(root, \"train.csv\"))\n\nprint(\"\\nFound train.csv files:\")\n\nfor f in found:\n    print(f)\n\nprint(\"\\nTotal found:\", len(found))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:54:15.725146Z","iopub.execute_input":"2026-08-08T08:54:15.725562Z","iopub.status.idle":"2026-08-08T08:55:47.629391Z","shell.execute_reply.started":"2026-08-08T08:54:15.725529Z","shell.execute_reply":"2026-08-08T08:55:47.628335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 2 — LOAD RSNA KNEE DATASET\n# ============================================\n\nimport os\nimport pandas as pd\n\n# Correct Kaggle dataset path\nbase_dir = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\nprint(\"Dataset folder:\")\nprint(base_dir)\n\nprint(\"\\nFiles and folders:\")\nprint(os.listdir(base_dir))\n\n# --------------------------------------------\n# CSV paths\n# --------------------------------------------\n\ntrain_csv_path = os.path.join(base_dir, \"train.csv\")\ntest_csv_path = os.path.join(base_dir, \"test.csv\")\ntrain_series_csv_path = os.path.join(base_dir, \"train_series.csv\")\ntest_series_csv_path = os.path.join(base_dir, \"test_series.csv\")\n\n# --------------------------------------------\n# Check files\n# --------------------------------------------\n\nprint(\"\\nFile check:\")\n\nfor path in [\n    train_csv_path,\n    test_csv_path,\n    train_series_csv_path,\n    test_series_csv_path\n]:\n    print(\n        os.path.basename(path),\n        \"->\",\n        os.path.exists(path)\n    )\n\n# --------------------------------------------\n# Load CSV files\n# --------------------------------------------\n\ntrain_df = pd.read_csv(train_csv_path)\ntest_df = pd.read_csv(test_csv_path)\ntrain_series_df = pd.read_csv(train_series_csv_path)\ntest_series_df = pd.read_csv(test_series_csv_path)\n\n# --------------------------------------------\n# Display information\n# --------------------------------------------\n\nprint(\"\\n============================================\")\nprint(\"DATASET LOADED SUCCESSFULLY\")\nprint(\"============================================\")\n\nprint(\"Train CSV:\", train_df.shape)\nprint(\"Test CSV:\", test_df.shape)\nprint(\"Train Series:\", train_series_df.shape)\nprint(\"Test Series:\", test_series_df.shape)\n\nprint(\"\\nTrain columns:\")\nprint(train_df.columns.tolist())\n\nprint(\"\\nFirst 5 training rows:\")\ndisplay(train_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:57:50.429214Z","iopub.execute_input":"2026-08-08T08:57:50.430312Z","iopub.status.idle":"2026-08-08T08:57:50.723475Z","shell.execute_reply.started":"2026-08-08T08:57:50.430258Z","shell.execute_reply":"2026-08-08T08:57:50.722469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 3 — PREPARE LABELED STUDIES\n# ============================================\n\nimport numpy as np\nimport pandas as pd\n\n# 12 abnormality classes\nlabel_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# Keep only studies having labels\n# --------------------------------------------\n\nlabeled_df = train_df.dropna(\n    subset=label_cols,\n    how=\"all\"\n).copy()\n\nprint(\"Total studies:\", len(train_df))\nprint(\"Labeled studies:\", len(labeled_df))\nprint(\"Expected labeled studies: 58\")\n\n# --------------------------------------------\n# Convert labels to numeric 0/1\n# --------------------------------------------\n\nfor col in label_cols:\n    labeled_df[col] = pd.to_numeric(\n        labeled_df[col],\n        errors=\"coerce\"\n    )\n\n# Remove any remaining rows without labels\nlabeled_df = labeled_df.dropna(\n    subset=label_cols,\n    how=\"any\"\n).copy()\n\n# Convert to float\nlabeled_df[label_cols] = labeled_df[label_cols].astype(np.float32)\n\n# --------------------------------------------\n# Check positive cases\n# --------------------------------------------\n\nprint(\"\\n============================================\")\nprint(\"POSITIVE CASES\")\nprint(\"============================================\")\n\nfor col in label_cols:\n    positives = int(labeled_df[col].sum())\n    negatives = int(len(labeled_df) - positives)\n\n    print(\n        f\"{col:20s} | \"\n        f\"Positive: {positives:2d} | \"\n        f\"Negative: {negatives:2d}\"\n    )\n\n# --------------------------------------------\n# Final label matrix\n# --------------------------------------------\n\nY = labeled_df[label_cols].values.astype(np.float32)\n\nprint(\"\\n============================================\")\nprint(\"LABEL MATRIX\")\nprint(\"============================================\")\n\nprint(\"Shape:\", Y.shape)\nprint(\"Number of classes:\", len(label_cols))\n\nprint(\"\\nFirst 5 labeled studies:\")\ndisplay(labeled_df[[\"StudyInstanceUID\"] + label_cols].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:58:25.721026Z","iopub.execute_input":"2026-08-08T08:58:25.722047Z","iopub.status.idle":"2026-08-08T08:58:25.767403Z","shell.execute_reply.started":"2026-08-08T08:58:25.722008Z","shell.execute_reply":"2026-08-08T08:58:25.766457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 4 — MATCH MRI STUDIES WITH LABELS\n# ============================================\n\nimport os\nimport glob\nimport pandas as pd\n\n# Dataset path\nbase_dir = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\n# Possible MRI folder locations\npossible_dirs = [\n    os.path.join(base_dir, \"train_series\"),\n    os.path.join(base_dir, \"test_series\")\n]\n\nprint(\"Checking MRI folders...\\n\")\n\nfor d in possible_dirs:\n    print(d, \"->\", os.path.exists(d))\n\n# --------------------------------------------\n# Find all study folders recursively\n# --------------------------------------------\n\nstudy_folders = {}\n\nfor root, dirs, files in os.walk(base_dir):\n\n    # A study folder is identified by its name\n    # matching a StudyInstanceUID\n    folder_name = os.path.basename(root)\n\n    if folder_name in set(labeled_df[\"StudyInstanceUID\"]):\n        study_folders[folder_name] = root\n\nprint(\"\\n============================================\")\nprint(\"MATCHING RESULT\")\nprint(\"============================================\")\n\nprint(\"Labeled studies:\", len(labeled_df))\nprint(\"Matched MRI studies:\", len(study_folders))\n\n# --------------------------------------------\n# Show unmatched studies\n# --------------------------------------------\n\nlabeled_ids = set(labeled_df[\"StudyInstanceUID\"])\nmatched_ids = set(study_folders.keys())\n\nunmatched = labeled_ids - matched_ids\n\nprint(\"Unmatched studies:\", len(unmatched))\n\nif len(unmatched) > 0:\n    print(\"\\nUnmatched IDs:\")\n    for uid in list(unmatched)[:10]:\n        print(uid)\n\n# --------------------------------------------\n# Create matched dataframe\n# --------------------------------------------\n\nmatched_df = labeled_df[\n    labeled_df[\"StudyInstanceUID\"].isin(matched_ids)\n].copy()\n\nmatched_df[\"study_path\"] = matched_df[\"StudyInstanceUID\"].map(\n    study_folders\n)\n\nprint(\"\\nMatched dataframe:\", matched_df.shape)\n\n# --------------------------------------------\n# Show first 5\n# --------------------------------------------\n\ndisplay(\n    matched_df[\n        [\"StudyInstanceUID\", \"study_path\"] + label_cols\n    ].head()\n)\n\n# --------------------------------------------\n# Count DICOM files per study\n# --------------------------------------------\n\ndef count_dicom_files(folder):\n    count = 0\n\n    for root, dirs, files in os.walk(folder):\n        count += sum(\n            1 for f in files\n            if f.lower().endswith(\".dcm\")\n        )\n\n    return count\n\n\nmatched_df[\"dicom_count\"] = matched_df[\"study_path\"].apply(\n    count_dicom_files\n)\n\nprint(\"\\nDICOM image counts:\")\ndisplay(\n    matched_df[\n        [\"StudyInstanceUID\", \"dicom_count\"]\n    ].head(10)\n)\n\nprint(\n    \"\\nTotal DICOM images:\",\n    matched_df[\"dicom_count\"].sum()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T08:59:07.889813Z","iopub.execute_input":"2026-08-08T08:59:07.890254Z","iopub.status.idle":"2026-08-08T09:00:31.58595Z","shell.execute_reply.started":"2026-08-08T08:59:07.890193Z","shell.execute_reply":"2026-08-08T09:00:31.584976Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 5 — DICOM DATASET\n# ============================================\n\nimport os\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n\n# DICOM library\ntry:\n    import pydicom\nexcept ImportError:\n    !pip install -q pydicom\n    import pydicom\n\nprint(\"PyTorch:\", torch.__version__)\nprint(\"pydicom:\", pydicom.__version__)\n\n# ============================================\n# SETTINGS\n# ============================================\n\nIMAGE_SIZE = 224\nIMAGES_PER_STUDY = 8\n\n# ============================================\n# DICOM READER\n# ============================================\n\ndef read_dicom(path):\n\n    ds = pydicom.dcmread(path)\n\n    image = ds.pixel_array.astype(np.float32)\n\n    # Handle MONOCHROME1\n    if getattr(ds, \"PhotometricInterpretation\", \"\") == \"MONOCHROME1\":\n        image = image.max() - image\n\n    # Normalize\n    image_min = image.min()\n    image_max = image.max()\n\n    if image_max > image_min:\n        image = (image - image_min) / (image_max - image_min)\n    else:\n        image = np.zeros_like(image)\n\n    # Convert to 8-bit\n    image = (image * 255).clip(0, 255).astype(np.uint8)\n\n    # Resize\n    image = Image.fromarray(image).convert(\"RGB\")\n    image = image.resize((IMAGE_SIZE, IMAGE_SIZE))\n\n    # Tensor\n    image = np.array(image).astype(np.float32) / 255.0\n\n    image = torch.from_numpy(image).permute(2, 0, 1)\n\n    return image\n\n\n# ============================================\n# GET DICOM FILES\n# ============================================\n\ndef get_dicom_files(study_path):\n\n    files = []\n\n    for root, dirs, filenames in os.walk(study_path):\n\n        for filename in filenames:\n\n            if filename.lower().endswith(\".dcm\"):\n                files.append(\n                    os.path.join(root, filename)\n                )\n\n    return sorted(files)\n\n\n# ============================================\n# SAMPLE IMAGES FROM STUDY\n# ============================================\n\ndef sample_dicom_files(files, n=8):\n\n    if len(files) == 0:\n        return []\n\n    if len(files) <= n:\n        return files\n\n    indexes = np.linspace(\n        0,\n        len(files) - 1,\n        n\n    ).astype(int)\n\n    return [files[i] for i in indexes]\n\n\n# ============================================\n# DATASET\n# ============================================\n\nclass KneeMRIDataset(Dataset):\n\n    def __init__(\n        self,\n        dataframe,\n        label_columns,\n        images_per_study=8\n    ):\n\n        self.df = dataframe.reset_index(drop=True)\n\n        self.label_columns = label_columns\n\n        self.images_per_study = images_per_study\n\n    def __len__(self):\n\n        return len(self.df)\n\n    def __getitem__(self, index):\n\n        row = self.df.iloc[index]\n\n        study_id = row[\"StudyInstanceUID\"]\n\n        study_path = row[\"study_path\"]\n\n        # Get all DICOMs\n        dicom_files = get_dicom_files(\n            study_path\n        )\n\n        # Sample evenly\n        dicom_files = sample_dicom_files(\n            dicom_files,\n            self.images_per_study\n        )\n\n        images = []\n\n        for file in dicom_files:\n\n            try:\n\n                image = read_dicom(file)\n\n                images.append(image)\n\n            except Exception as e:\n\n                print(\n                    \"DICOM read error:\",\n                    file,\n                    e\n                )\n\n        # If some images failed\n        if len(images) == 0:\n\n            images = [\n                torch.zeros(\n                    3,\n                    IMAGE_SIZE,\n                    IMAGE_SIZE\n                )\n            ]\n\n        # Stack images\n        images = torch.stack(images)\n\n        # Labels\n        labels = torch.tensor(\n            row[self.label_columns].values.astype(\n                np.float32\n            )\n        )\n\n        return {\n            \"images\": images,\n            \"labels\": labels,\n            \"study_id\": study_id\n        }\n\n\n# ============================================\n# CREATE DATASET\n# ============================================\n\ndataset = KneeMRIDataset(\n    matched_df,\n    label_cols,\n    images_per_study=IMAGES_PER_STUDY\n)\n\nprint(\"\\n============================================\")\nprint(\"DATASET CREATED\")\nprint(\"============================================\")\n\nprint(\"Number of studies:\", len(dataset))\nprint(\"Images per study:\", IMAGES_PER_STUDY)\nprint(\"Image size:\", IMAGE_SIZE)\nprint(\"Number of labels:\", len(label_cols))\n\n\n# ============================================\n# TEST ONE STUDY\n# ============================================\n\nsample = dataset[0]\n\nprint(\"\\n============================================\")\nprint(\"FIRST STUDY TEST\")\nprint(\"============================================\")\n\nprint(\"Study ID:\")\nprint(sample[\"study_id\"])\n\nprint(\"\\nImage tensor shape:\")\nprint(sample[\"images\"].shape)\n\nprint(\"\\nLabels:\")\nprint(sample[\"labels\"])\n\nprint(\"\\nDataset test successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T09:01:33.262351Z","iopub.execute_input":"2026-08-08T09:01:33.263169Z","iopub.status.idle":"2026-08-08T09:01:33.591046Z","shell.execute_reply.started":"2026-08-08T09:01:33.263134Z","shell.execute_reply":"2026-08-08T09:01:33.590133Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 6 — TRAIN / VALIDATION SPLIT\n# ============================================\n\nfrom sklearn.model_selection import train_test_split\nfrom torch.utils.data import DataLoader\n\n# --------------------------------------------\n# Split 80% training / 20% validation\n# --------------------------------------------\n\ntrain_df, val_df = train_test_split(\n    matched_df,\n    test_size=0.20,\n    random_state=42,\n    shuffle=True\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"============================================\")\nprint(\"TRAIN / VALIDATION SPLIT\")\nprint(\"============================================\")\n\nprint(\"Total studies:\", len(matched_df))\nprint(\"Training studies:\", len(train_df))\nprint(\"Validation studies:\", len(val_df))\n\n# --------------------------------------------\n# Create datasets\n# --------------------------------------------\n\ntrain_dataset = KneeMRIDataset(\n    train_df,\n    label_cols,\n    images_per_study=IMAGES_PER_STUDY\n)\n\nval_dataset = KneeMRIDataset(\n    val_df,\n    label_cols,\n    images_per_study=IMAGES_PER_STUDY\n)\n\n# --------------------------------------------\n# DataLoaders\n# CPU-friendly settings\n# --------------------------------------------\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=2,\n    shuffle=True,\n    num_workers=0\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=2,\n    shuffle=False,\n    num_workers=0\n)\n\nprint(\"\\n============================================\")\nprint(\"DATALOADERS READY\")\nprint(\"============================================\")\n\nprint(\"Training batches:\", len(train_loader))\nprint(\"Validation batches:\", len(val_loader))\n\n# --------------------------------------------\n# Test one batch\n# --------------------------------------------\n\nbatch = next(iter(train_loader))\n\nprint(\"\\n============================================\")\nprint(\"BATCH TEST\")\nprint(\"============================================\")\n\nprint(\"Images shape:\", batch[\"images\"].shape)\nprint(\"Labels shape:\", batch[\"labels\"].shape)\n\nprint(\"\\nExpected:\")\nprint(\"Images -> [batch, 8, 3, 224, 224]\")\nprint(\"Labels -> [batch, 12]\")\n\nprint(\"\\nStep 6 successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T09:02:21.280001Z","iopub.execute_input":"2026-08-08T09:02:21.280376Z","iopub.status.idle":"2026-08-08T09:02:21.70022Z","shell.execute_reply.started":"2026-08-08T09:02:21.280343Z","shell.execute_reply":"2026-08-08T09:02:21.699354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 7 — RESNET18 MODEL\n# ============================================\n\nimport torch\nimport torch.nn as nn\nfrom torchvision import models\n\n# --------------------------------------------\n# Device\n# --------------------------------------------\n\ndevice = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Device:\", device)\n\n# --------------------------------------------\n# Create ResNet18\n# --------------------------------------------\n\nmodel = models.resnet18(weights=None)\n\n# --------------------------------------------\n# Change final layer\n# 12 abnormalities\n# --------------------------------------------\n\nnum_features = model.fc.in_features\n\nmodel.fc = nn.Linear(\n    num_features,\n    len(label_cols)\n)\n\n# --------------------------------------------\n# Move model to device\n# --------------------------------------------\n\nmodel = model.to(device)\n\n# --------------------------------------------\n# Check model\n# --------------------------------------------\n\nprint(\"\\n============================================\")\nprint(\"MODEL READY\")\nprint(\"============================================\")\n\nprint(\"Architecture: ResNet18\")\nprint(\"Input channels: 3\")\nprint(\"Output classes:\", len(label_cols))\nprint(\"Final layer:\", model.fc)\n\nprint(\"\\nDevice:\", next(model.parameters()).device)\n\nprint(\"\\nModel successfully created!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T09:02:58.158447Z","iopub.execute_input":"2026-08-08T09:02:58.158807Z","iopub.status.idle":"2026-08-08T09:02:58.350526Z","shell.execute_reply.started":"2026-08-08T09:02:58.158776Z","shell.execute_reply":"2026-08-08T09:02:58.349546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 8 — WEIGHTED LOSS + ADAMW\n# ============================================\n\nimport torch\nimport torch.nn as nn\n\n# --------------------------------------------\n# Calculate positive weights from TRAINING data\n# --------------------------------------------\n\npositive_weights = []\n\nprint(\"============================================\")\nprint(\"CLASS WEIGHTS\")\nprint(\"============================================\")\n\nfor col in label_cols:\n\n    positives = train_df[col].sum()\n    negatives = len(train_df) - positives\n\n    # Avoid division by zero\n    if positives > 0:\n        weight = negatives / positives\n    else:\n        weight = 1.0\n\n    positive_weights.append(weight)\n\n    print(\n        f\"{col:20s} | \"\n        f\"Positive: {int(positives):2d} | \"\n        f\"Negative: {int(negatives):2d} | \"\n        f\"Weight: {weight:.2f}\"\n    )\n\n# --------------------------------------------\n# Convert weights to Tensor\n# --------------------------------------------\n\npos_weight = torch.tensor(\n    positive_weights,\n    dtype=torch.float32,\n    device=device\n)\n\n# --------------------------------------------\n# Weighted BCE Loss\n# --------------------------------------------\n\ncriterion = nn.BCEWithLogitsLoss(\n    pos_weight=pos_weight\n)\n\n# --------------------------------------------\n# AdamW Optimizer\n# --------------------------------------------\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=0.00005,\n    weight_decay=0.01\n)\n\n# --------------------------------------------\n# Training setup\n# --------------------------------------------\n\nprint(\"\\n============================================\")\nprint(\"TRAINING SETUP READY\")\nprint(\"============================================\")\n\nprint(\"Loss: Weighted BCEWithLogitsLoss\")\nprint(\"Optimizer: AdamW\")\nprint(\"Learning rate: 0.00005\")\nprint(\"Weight decay: 0.01\")\n\nprint(\"\\nPositive weights:\")\nprint(pos_weight)\n\nprint(\"\\nStep 8 successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:29:26.982085Z","iopub.execute_input":"2026-08-08T10:29:26.982348Z","iopub.status.idle":"2026-08-08T10:29:26.992778Z","shell.execute_reply.started":"2026-08-08T10:29:26.982318Z","shell.execute_reply":"2026-08-08T10:29:26.991698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 8 — WEIGHTED LOSS + ADAMW\n# SELF-CONTAINED VERSION\n# ============================================\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torchvision import models\n\n# ============================================\n# 1. DEVICE\n# ============================================\n\ndevice = torch.device(\n    \"cuda\" if torch.cuda.is_available() else \"cpu\"\n)\n\nprint(\"Device:\", device)\n\n\n# ============================================\n# 2. LABEL COLUMNS\n# ============================================\n\nlabel_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\nprint(\"Number of labels:\", len(label_cols))\n\n\n# ============================================\n# 3. DATASET PATH\n# ============================================\n\nbase_dir = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\ntrain_csv_path = os.path.join(\n    base_dir,\n    \"train.csv\"\n)\n\n# Load train CSV\ntrain_all_df = pd.read_csv(train_csv_path)\n\n\n# ============================================\n# 4. GET LABELED STUDIES\n# ============================================\n\nlabeled_df = train_all_df.dropna(\n    subset=label_cols,\n    how=\"all\"\n).copy()\n\n# Convert labels to numeric\nfor col in label_cols:\n    labeled_df[col] = pd.to_numeric(\n        labeled_df[col],\n        errors=\"coerce\"\n    )\n\n# Keep only complete labels\nlabeled_df = labeled_df.dropna(\n    subset=label_cols,\n    how=\"any\"\n).copy()\n\nlabeled_df[label_cols] = labeled_df[\n    label_cols\n].astype(np.float32)\n\n\n# ============================================\n# 5. TRAIN / VALIDATION SPLIT\n# ============================================\n\nfrom sklearn.model_selection import train_test_split\n\ntrain_df, val_df = train_test_split(\n    labeled_df,\n    test_size=0.20,\n    random_state=42,\n    shuffle=True\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\n\nprint(\"\\n============================================\")\nprint(\"DATA\")\nprint(\"============================================\")\n\nprint(\"Total labeled studies:\", len(labeled_df))\nprint(\"Training studies:\", len(train_df))\nprint(\"Validation studies:\", len(val_df))\n\n\n# ============================================\n# 6. CALCULATE POSITIVE WEIGHTS\n# ============================================\n\npositive_weights = []\n\nprint(\"\\n============================================\")\nprint(\"CLASS WEIGHTS\")\nprint(\"============================================\")\n\nfor col in label_cols:\n\n    positives = float(train_df[col].sum())\n    negatives = float(len(train_df) - positives)\n\n    if positives > 0:\n        weight = negatives / positives\n    else:\n        weight = 1.0\n\n    positive_weights.append(weight)\n\n    print(\n        f\"{col:20s} | \"\n        f\"Positive: {int(positives):2d} | \"\n        f\"Negative: {int(negatives):2d} | \"\n        f\"Weight: {weight:.2f}\"\n    )\n\n\n# ============================================\n# 7. POSITIVE WEIGHT TENSOR\n# ============================================\n\npos_weight = torch.tensor(\n    positive_weights,\n    dtype=torch.float32,\n    device=device\n)\n\n\n# ============================================\n# 8. CREATE RESNET18\n# ============================================\n\nmodel = models.resnet18(weights=None)\n\nnum_features = model.fc.in_features\n\nmodel.fc = nn.Linear(\n    num_features,\n    len(label_cols)\n)\n\nmodel = model.to(device)\n\n\n# ============================================\n# 9. WEIGHTED BCE LOSS\n# ============================================\n\ncriterion = nn.BCEWithLogitsLoss(\n    pos_weight=pos_weight\n)\n\n\n# ============================================\n# 10. ADAMW OPTIMIZER\n# ============================================\n\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=0.00005,\n    weight_decay=0.01\n)\n\n\n# ============================================\n# 11. FINAL CHECK\n# ============================================\n\nprint(\"\\n============================================\")\nprint(\"TRAINING SETUP READY\")\nprint(\"============================================\")\n\nprint(\"Model: ResNet18\")\nprint(\"Output classes:\", len(label_cols))\nprint(\"Loss: Weighted BCEWithLogitsLoss\")\nprint(\"Optimizer: AdamW\")\nprint(\"Learning rate:\", 0.00005)\nprint(\"Weight decay:\", 0.01)\nprint(\"Device:\", device)\n\nprint(\"\\nPositive weights:\")\nprint(pos_weight)\n\nprint(\"\\nStep 8 successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:48:28.233417Z","iopub.execute_input":"2026-08-08T10:48:28.234002Z","iopub.status.idle":"2026-08-08T10:48:32.708128Z","shell.execute_reply.started":"2026-08-08T10:48:28.233974Z","shell.execute_reply":"2026-08-08T10:48:32.707073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 9 — MODEL TRAINING\n# ============================================================\n\nimport torch\nimport numpy as np\nfrom sklearn.metrics import roc_auc_score\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")\n\nEPOCHS = 10\n\nprint(\"============================================\")\nprint(\"TRAINING شروع ٿي رهي آهي\")\nprint(\"============================================\")\nprint(f\"Epochs: {EPOCHS}\")\nprint(f\"Device: {device}\")\nprint()\n\nbest_val_loss = float(\"inf\")\n\nfor epoch in range(EPOCHS):\n\n    # =========================\n    # TRAINING\n    # =========================\n    model.train()\n\n    train_loss = 0.0\n    train_samples = 0\n\n    for images, labels in train_loader:\n\n        images = images.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n\n        # [batch, 8, 3, 224, 224]\n        batch_size, num_images, channels, height, width = images.shape\n\n        # 8 MRI images کي ResNet لاءِ تيار ڪرڻ\n        images = images.view(\n            batch_size * num_images,\n            channels,\n            height,\n            width\n        )\n\n        outputs = model(images)\n\n        # واپس study جي حساب سان ترتيب ڏيڻ\n        outputs = outputs.view(\n            batch_size,\n            num_images,\n            len(label_cols)\n        )\n\n        # 8 images جي prediction جو average\n        outputs = outputs.mean(dim=1)\n\n        loss = criterion(outputs, labels)\n\n        loss.backward()\n        optimizer.step()\n\n        train_loss += loss.item() * batch_size\n        train_samples += batch_size\n\n    train_loss /= train_samples\n\n    # =========================\n    # VALIDATION\n    # =========================\n    model.eval()\n\n    val_loss = 0.0\n    val_samples = 0\n\n    all_preds = []\n    all_labels = []\n\n    with torch.no_grad():\n\n        for images, labels in val_loader:\n\n            images = images.to(device)\n            labels = labels.to(device)\n\n            batch_size, num_images, channels, height, width = images.shape\n\n            images = images.view(\n                batch_size * num_images,\n                channels,\n                height,\n                width\n            )\n\n            outputs = model(images)\n\n            outputs = outputs.view(\n                batch_size,\n                num_images,\n                len(label_cols)\n            )\n\n            outputs = outputs.mean(dim=1)\n\n            loss = criterion(outputs, labels)\n\n            val_loss += loss.item() * batch_size\n            val_samples += batch_size\n\n            probabilities = torch.sigmoid(outputs)\n\n            all_preds.append(\n                probabilities.cpu().numpy()\n            )\n\n            all_labels.append(\n                labels.cpu().numpy()\n            )\n\n    val_loss /= val_samples\n\n    all_preds = np.concatenate(all_preds, axis=0)\n    all_labels = np.concatenate(all_labels, axis=0)\n\n    # =========================\n    # AUC\n    # =========================\n    auc_scores = []\n\n    for i, col in enumerate(label_cols):\n\n        y_true = all_labels[:, i]\n        y_score = all_preds[:, i]\n\n        # AUC تڏهن ئي calculate ٿيندو\n        # جڏهن 0 ۽ 1 ٻئي موجود هجن\n        if len(np.unique(y_true)) == 2:\n\n            auc = roc_auc_score(\n                y_true,\n                y_score\n            )\n\n            auc_scores.append(auc)\n\n    if len(auc_scores) > 0:\n        mean_auc = np.mean(auc_scores)\n    else:\n        mean_auc = float(\"nan\")\n\n    # =========================\n    # RESULT\n    # =========================\n    print(\n        f\"Epoch {epoch+1:02d}/{EPOCHS} | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Val Loss: {val_loss:.4f} | \"\n        f\"Mean AUC: {mean_auc:.4f}\"\n    )\n\n    # =========================\n    # BEST MODEL SAVE\n    # =========================\n    if val_loss < best_val_loss:\n\n        best_val_loss = val_loss\n\n        torch.save(\n            {\n                \"model_state_dict\": model.state_dict(),\n                \"optimizer_state_dict\": optimizer.state_dict(),\n                \"epoch\": epoch + 1,\n                \"val_loss\": val_loss,\n                \"label_cols\": label_cols\n            },\n            \"/kaggle/working/rsna_knee_best_model.pth\"\n        )\n\n        print(\"  ✓ بهترين Model محفوظ ٿي ويو!\")\n\nprint()\nprint(\"============================================\")\nprint(\"TRAINING مڪمل ٿي وئي\")\nprint(\"============================================\")\n\nprint(f\"Best Validation Loss: {best_val_loss:.4f}\")\n\nprint()\nprint(\"Model هتي محفوظ ٿيو:\")\nprint(\"/kaggle/working/rsna_knee_best_model.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:49:40.806479Z","iopub.execute_input":"2026-08-08T10:49:40.807081Z","iopub.status.idle":"2026-08-08T10:49:40.826876Z","shell.execute_reply.started":"2026-08-08T10:49:40.807044Z","shell.execute_reply":"2026-08-08T10:49:40.825524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 8.5 — RESTORE / CREATE DATALOADERS\n# ============================================================\n\nfrom torch.utils.data import DataLoader\n\nBATCH_SIZE = 2\n\n# Train Dataset\ntrain_dataset = KneeMRIDataset(\n    train_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\n# Validation Dataset\nval_dataset = KneeMRIDataset(\n    val_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\n# DataLoaders\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=0\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=0\n)\n\nprint(\"============================================\")\nprint(\"DATALOADERS RESTORED\")\nprint(\"============================================\")\n\nprint(\"Training studies:\", len(train_dataset))\nprint(\"Validation studies:\", len(val_dataset))\n\nprint(\"Training batches:\", len(train_loader))\nprint(\"Validation batches:\", len(val_loader))\n\n# Test\nimages, labels = next(iter(train_loader))\n\nprint()\nprint(\"BATCH TEST\")\nprint(\"Images shape:\", images.shape)\nprint(\"Labels shape:\", labels.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:50:23.543853Z","iopub.execute_input":"2026-08-08T10:50:23.544128Z","iopub.status.idle":"2026-08-08T10:50:23.555142Z","shell.execute_reply.started":"2026-08-08T10:50:23.544108Z","shell.execute_reply":"2026-08-08T10:50:23.553325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 6.5 — RECREATE MRI DATASET CLASS\n# ============================================================\n\nimport os\nimport numpy as np\nimport torch\nimport pydicom\n\nfrom torch.utils.data import Dataset\nfrom PIL import Image\n\n\nclass KneeMRIDataset(Dataset):\n\n    def __init__(\n        self,\n        df,\n        series_root,\n        label_cols,\n        num_images=8,\n        image_size=224\n    ):\n\n        self.df = df.reset_index(drop=True)\n        self.series_root = series_root\n        self.label_cols = label_cols\n        self.num_images = num_images\n        self.image_size = image_size\n\n    def __len__(self):\n        return len(self.df)\n\n    def load_dicom(self, filepath):\n\n        ds = pydicom.dcmread(filepath)\n\n        image = ds.pixel_array.astype(np.float32)\n\n        # Normalize\n        image -= image.min()\n\n        if image.max() > 0:\n            image /= image.max()\n\n        image = (image * 255).astype(np.uint8)\n\n        # PIL\n        image = Image.fromarray(image)\n\n        # Resize\n        image = image.resize(\n            (self.image_size, self.image_size)\n        )\n\n        image = np.array(image).astype(np.float32) / 255.0\n\n        # 1 channel -> 3 channels\n        image = np.stack(\n            [image, image, image],\n            axis=0\n        )\n\n        return torch.tensor(\n            image,\n            dtype=torch.float32\n        )\n\n    def __getitem__(self, idx):\n\n        row = self.df.iloc[idx]\n\n        study_id = str(\n            row[\"StudyInstanceUID\"]\n        )\n\n        study_path = os.path.join(\n            self.series_root,\n            study_id\n        )\n\n        # Find all series folders\n        series_folders = []\n\n        if os.path.exists(study_path):\n\n            for name in os.listdir(study_path):\n\n                full_path = os.path.join(\n                    study_path,\n                    name\n                )\n\n                if os.path.isdir(full_path):\n                    series_folders.append(full_path)\n\n        # Find DICOM files\n        dicom_files = []\n\n        for folder in series_folders:\n\n            for filename in os.listdir(folder):\n\n                if filename.lower().endswith(\".dcm\"):\n\n                    dicom_files.append(\n                        os.path.join(\n                            folder,\n                            filename\n                        )\n                    )\n\n        # Sort\n        dicom_files.sort()\n\n        # جيڪڏهن images گهٽ هجن\n        if len(dicom_files) == 0:\n\n            raise FileNotFoundError(\n                f\"No DICOM images found for study: {study_id}\"\n            )\n\n        # 8 images select ڪرڻ\n        if len(dicom_files) >= self.num_images:\n\n            indices = np.linspace(\n                0,\n                len(dicom_files) - 1,\n                self.num_images\n            ).astype(int)\n\n            selected_files = [\n                dicom_files[i]\n                for i in indices\n            ]\n\n        else:\n\n            selected_files = dicom_files.copy()\n\n            while len(selected_files) < self.num_images:\n\n                selected_files.append(\n                    dicom_files[-1]\n                )\n\n        # Load images\n        images = []\n\n        for filepath in selected_files:\n\n            image = self.load_dicom(\n                filepath\n            )\n\n            images.append(image)\n\n        images = torch.stack(images)\n\n        # Labels\n        labels = torch.tensor(\n            row[self.label_cols].values.astype(\n                np.float32\n            ),\n            dtype=torch.float32\n        )\n\n        return images, labels\n\n\nprint(\"============================================\")\nprint(\"KneeMRIDataset CREATED\")\nprint(\"============================================\")\n\nprint(\"Number of labels:\", len(label_cols))\nprint(\"Images per study:\", 8)\nprint(\"Image size:\", 224)\n\nprint()\nprint(\"Dataset class ready!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:50:55.676745Z","iopub.execute_input":"2026-08-08T10:50:55.67707Z","iopub.status.idle":"2026-08-08T10:50:56.208653Z","shell.execute_reply.started":"2026-08-08T10:50:55.677047Z","shell.execute_reply":"2026-08-08T10:50:56.207512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# DATASET TEST\n# ============================================================\n\ntrain_dataset = KneeMRIDataset(\n    train_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\nval_dataset = KneeMRIDataset(\n    val_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\nprint(\"Training studies:\", len(train_dataset))\nprint(\"Validation studies:\", len(val_dataset))\n\nimages, labels = train_dataset[0]\n\nprint()\nprint(\"First study test:\")\nprint(\"Images shape:\", images.shape)\nprint(\"Labels shape:\", labels.shape)\nprint(\"Labels:\", labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:51:54.418614Z","iopub.execute_input":"2026-08-08T10:51:54.419026Z","iopub.status.idle":"2026-08-08T10:51:54.42904Z","shell.execute_reply.started":"2026-08-08T10:51:54.418996Z","shell.execute_reply":"2026-08-08T10:51:54.427867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 6.6 — DEFINE PATHS + DATASET TEST\n# ============================================================\n\nimport os\nimport torch\n\n# MRI series folder\nseries_root = \"/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series\"\n\n# 12 labels\nlabel_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\nprint(\"============================================\")\nprint(\"PATHS / LABELS\")\nprint(\"============================================\")\n\nprint(\"Series root:\", series_root)\nprint(\"Series folder exists:\", os.path.exists(series_root))\nprint(\"Number of labels:\", len(label_cols))\n\n# ------------------------------------------------------------\n# CREATE TRAIN DATASET\n# ------------------------------------------------------------\n\ntrain_dataset = KneeMRIDataset(\n    train_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\n# ------------------------------------------------------------\n# CREATE VALIDATION DATASET\n# ------------------------------------------------------------\n\nval_dataset = KneeMRIDataset(\n    val_df,\n    series_root,\n    label_cols,\n    num_images=8,\n    image_size=224\n)\n\nprint()\nprint(\"============================================\")\nprint(\"DATASET TEST\")\nprint(\"============================================\")\n\nprint(\"Training studies:\", len(train_dataset))\nprint(\"Validation studies:\", len(val_dataset))\n\n# ------------------------------------------------------------\n# TEST FIRST TRAINING STUDY\n# ------------------------------------------------------------\n\nimages, labels = train_dataset[0]\n\nprint()\nprint(\"FIRST STUDY TEST\")\nprint(\"Images shape:\", images.shape)\nprint(\"Labels shape:\", labels.shape)\nprint(\"Labels:\", labels)\n\nprint()\nprint(\"============================================\")\nprint(\"DATASET TEST SUCCESSFUL!\")\nprint(\"============================================\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:55:17.906344Z","iopub.execute_input":"2026-08-08T10:55:17.906668Z","iopub.status.idle":"2026-08-08T10:55:18.081225Z","shell.execute_reply.started":"2026-08-08T10:55:17.906643Z","shell.execute_reply":"2026-08-08T10:55:18.080153Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import DataLoader\n\n# ============================================\n# DATALOADERS\n# ============================================\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=2,\n    shuffle=True,\n    num_workers=0\n)\n\nval_loader = DataLoader(\n    val_dataset,\n    batch_size=2,\n    shuffle=False,\n    num_workers=0\n)\n\nprint(\"============================================\")\nprint(\"DATALOADERS READY\")\nprint(\"============================================\")\n\nprint(\"Training batches:\", len(train_loader))\nprint(\"Validation batches:\", len(val_loader))\n\n# ============================================\n# BATCH TEST\n# ============================================\n\nimages, labels = next(iter(train_loader))\n\nprint(\"\\n============================================\")\nprint(\"BATCH TEST\")\nprint(\"============================================\")\n\nprint(\"Images shape:\", images.shape)\nprint(\"Labels shape:\", labels.shape)\n\nprint(\"\\nExpected:\")\nprint(\"Images -> [batch, 8, 3, 224, 224]\")\nprint(\"Labels -> [batch, 12]\")\n\nprint(\"\\nStep 10 successful!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T10:56:05.542814Z","iopub.execute_input":"2026-08-08T10:56:05.543133Z","iopub.status.idle":"2026-08-08T10:56:05.927735Z","shell.execute_reply.started":"2026-08-08T10:56:05.543111Z","shell.execute_reply":"2026-08-08T10:56:05.926655Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 11 - MODEL TRAINING\n# ============================================\n\nimport torch\nimport numpy as np\n\nEPOCHS = 10\n\nbest_val_loss = float(\"inf\")\n\ntrain_losses = []\nval_losses = []\n\nprint(\"============================================\")\nprint(\"TRAINING STARTED\")\nprint(\"============================================\")\n\nfor epoch in range(EPOCHS):\n\n    # ========================================\n    # TRAIN\n    # ========================================\n\n    model.train()\n\n    running_train_loss = 0.0\n    train_samples = 0\n\n    for images, labels in train_loader:\n\n        images = images.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n\n        # images:\n        # [batch, 8, 3, 224, 224]\n\n        batch_size = images.size(0)\n        num_images = images.size(1)\n\n        # Combine study images into batch\n        images = images.view(\n            batch_size * num_images,\n            3,\n            224,\n            224\n        )\n\n        # Model prediction\n        outputs = model(images)\n\n        # Average 8 image predictions\n        outputs = outputs.view(\n            batch_size,\n            num_images,\n            -1\n        ).mean(dim=1)\n\n        # Loss\n        loss = criterion(outputs, labels)\n\n        loss.backward()\n\n        optimizer.step()\n\n        running_train_loss += loss.item() * batch_size\n        train_samples += batch_size\n\n    train_loss = running_train_loss / train_samples\n\n    # ========================================\n    # VALIDATION\n    # ========================================\n\n    model.eval()\n\n    running_val_loss = 0.0\n    val_samples = 0\n\n    with torch.no_grad():\n\n        for images, labels in val_loader:\n\n            images = images.to(device)\n            labels = labels.to(device)\n\n            batch_size = images.size(0)\n            num_images = images.size(1)\n\n            images = images.view(\n                batch_size * num_images,\n                3,\n                224,\n                224\n            )\n\n            outputs = model(images)\n\n            outputs = outputs.view(\n                batch_size,\n                num_images,\n                -1\n            ).mean(dim=1)\n\n            loss = criterion(outputs, labels)\n\n            running_val_loss += loss.item() * batch_size\n            val_samples += batch_size\n\n    val_loss = running_val_loss / val_samples\n\n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n\n    # ========================================\n    # SAVE BEST MODEL\n    # ========================================\n\n    if val_loss < best_val_loss:\n\n        best_val_loss = val_loss\n\n        torch.save(\n            model.state_dict(),\n            \"/kaggle/working/best_knee_resnet18.pth\"\n        )\n\n        best_text = \" <-- BEST MODEL SAVED\"\n\n    else:\n        best_text = \"\"\n\n    print(\n        f\"Epoch [{epoch+1}/{EPOCHS}] | \"\n        f\"Train Loss: {train_loss:.4f} | \"\n        f\"Val Loss: {val_loss:.4f}\"\n        f\"{best_text}\"\n    )\n\nprint(\"\\n============================================\")\nprint(\"TRAINING COMPLETED\")\nprint(\"============================================\")\n\nprint(\"Best validation loss:\", best_val_loss)\nprint(\"Best model saved at:\")\nprint(\"/kaggle/working/best_knee_resnet18.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:18:29.741868Z","iopub.execute_input":"2026-08-08T11:18:29.742148Z","iopub.status.idle":"2026-08-08T11:25:16.553387Z","shell.execute_reply.started":"2026-08-08T11:18:29.742125Z","shell.execute_reply":"2026-08-08T11:25:16.551433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 12 — LOAD BEST MODEL & PREDICTIONS\n# ============================================\n\nimport torch\nimport numpy as np\nimport pandas as pd\n\nprint(\"============================================\")\nprint(\"STEP 12 — LOADING BEST MODEL\")\nprint(\"============================================\")\n\n# Best model path\nbest_model_path = \"/kaggle/working/best_knee_resnet18.pth\"\n\n# Load best model\nmodel.load_state_dict(\n    torch.load(best_model_path, map_location=device)\n)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Best model loaded successfully!\")\nprint(\"Model path:\", best_model_path)\nprint(\"Device:\", device)\n\n# ============================================\n# VALIDATION PREDICTIONS\n# ============================================\n\nall_probs = []\nall_true = []\n\nwith torch.no_grad():\n\n    for images, labels in val_loader:\n\n        images = images.to(device)\n        labels = labels.to(device)\n\n        # Shape:\n        # images = [batch, 8, 3, 224, 224]\n\n        batch_size = images.size(0)\n\n        # Combine batch and image dimensions\n        images = images.view(\n            batch_size * images.size(1),\n            images.size(2),\n            images.size(3),\n            images.size(4)\n        )\n\n        # ResNet prediction for each MRI image\n        outputs = model(images)\n\n        # Convert logits to probabilities\n        probs = torch.sigmoid(outputs)\n\n        # Reshape:\n        # [batch*8, 12] -> [batch, 8, 12]\n        probs = probs.view(\n            batch_size,\n            8,\n            len(label_cols)\n        )\n\n        # Average 8 MRI images\n        study_probs = probs.mean(dim=1)\n\n        all_probs.append(study_probs.cpu().numpy())\n        all_true.append(labels.cpu().numpy())\n\n\n# ============================================\n# COMBINE RESULTS\n# ============================================\n\ny_prob = np.concatenate(all_probs, axis=0)\ny_true = np.concatenate(all_true, axis=0)\n\nprint()\nprint(\"============================================\")\nprint(\"PREDICTION RESULTS\")\nprint(\"============================================\")\n\nprint(\"Prediction shape:\", y_prob.shape)\nprint(\"True labels shape:\", y_true.shape)\n\nprint(\"Validation studies:\", len(y_prob))\nprint(\"Number of labels:\", len(label_cols))\n\n# ============================================\n# CHECK\n# ============================================\n\nassert y_prob.shape == y_true.shape\n\nprint()\nprint(\"Prediction test successful!\")\n\n# ============================================\n# AVERAGE PROBABILITY\n# ============================================\n\nprint()\nprint(\"============================================\")\nprint(\"AVERAGE PREDICTION PROBABILITY\")\nprint(\"============================================\")\n\nfor i, col in enumerate(label_cols):\n\n    avg_prob = y_prob[:, i].mean()\n\n    print(f\"{col:20s}: {avg_prob:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:29:27.244806Z","iopub.execute_input":"2026-08-08T11:29:27.24519Z","iopub.status.idle":"2026-08-08T11:29:30.441398Z","shell.execute_reply.started":"2026-08-08T11:29:27.245167Z","shell.execute_reply":"2026-08-08T11:29:30.440464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 13 — MODEL PERFORMANCE\n# ============================================\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.metrics import (\n    roc_auc_score,\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score\n)\n\nprint(\"============================================\")\nprint(\"STEP 13 — MODEL PERFORMANCE\")\nprint(\"============================================\")\n\n# --------------------------------------------\n# Threshold\n# --------------------------------------------\n\nthreshold = 0.5\n\n# Convert probabilities to 0/1 predictions\ny_pred = (y_prob >= threshold).astype(int)\n\n# --------------------------------------------\n# Results storage\n# --------------------------------------------\n\nresults = []\n\nprint()\nprint(\"=\" * 85)\nprint(\n    f\"{'Abnormality':20s} | \"\n    f\"{'AUC':>7s} | \"\n    f\"{'Accuracy':>8s} | \"\n    f\"{'Precision':>9s} | \"\n    f\"{'Recall':>7s} | \"\n    f\"{'F1':>7s}\"\n)\nprint(\"=\" * 85)\n\nfor i, col in enumerate(label_cols):\n\n    true = y_true[:, i]\n    pred = y_pred[:, i]\n    prob = y_prob[:, i]\n\n    # ----------------------------------------\n    # AUC\n    # ----------------------------------------\n\n    try:\n\n        if len(np.unique(true)) == 2:\n            auc = roc_auc_score(true, prob)\n        else:\n            auc = np.nan\n\n    except Exception:\n        auc = np.nan\n\n    # ----------------------------------------\n    # Classification metrics\n    # ----------------------------------------\n\n    accuracy = accuracy_score(true, pred)\n\n    precision = precision_score(\n        true,\n        pred,\n        zero_division=0\n    )\n\n    recall = recall_score(\n        true,\n        pred,\n        zero_division=0\n    )\n\n    f1 = f1_score(\n        true,\n        pred,\n        zero_division=0\n    )\n\n    results.append({\n        \"Abnormality\": col,\n        \"AUC\": auc,\n        \"Accuracy\": accuracy,\n        \"Precision\": precision,\n        \"Recall\": recall,\n        \"F1\": f1\n    })\n\n    auc_text = \"N/A\" if np.isnan(auc) else f\"{auc:.4f}\"\n\n    print(\n        f\"{col:20s} | \"\n        f\"{auc_text:>7s} | \"\n        f\"{accuracy:8.3f} | \"\n        f\"{precision:9.3f} | \"\n        f\"{recall:7.3f} | \"\n        f\"{f1:7.3f}\"\n    )\n\nprint(\"=\" * 85)\n\n# --------------------------------------------\n# DataFrame\n# --------------------------------------------\n\nperformance_df = pd.DataFrame(results)\n\nprint()\nprint(\"Performance table created successfully!\")\n\n# --------------------------------------------\n# Save results\n# --------------------------------------------\n\nperformance_path = \"/kaggle/working/model_performance.csv\"\n\nperformance_df.to_csv(\n    performance_path,\n    index=False\n)\n\nprint()\nprint(\"Saved at:\")\nprint(performance_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:30:08.423043Z","iopub.execute_input":"2026-08-08T11:30:08.42331Z","iopub.status.idle":"2026-08-08T11:30:08.512303Z","shell.execute_reply.started":"2026-08-08T11:30:08.42329Z","shell.execute_reply":"2026-08-08T11:30:08.511309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 14 — BEST THRESHOLD OPTIMIZATION\n# ============================================\n\nimport numpy as np\nimport pandas as pd\n\nfrom sklearn.metrics import (\n    precision_score,\n    recall_score,\n    f1_score,\n    accuracy_score\n)\n\nprint(\"============================================\")\nprint(\"STEP 14 — THRESHOLD OPTIMIZATION\")\nprint(\"============================================\")\n\nthresholds = np.arange(0.10, 0.91, 0.05)\n\nthreshold_results = []\n\nfor i, col in enumerate(label_cols):\n\n    true = y_true[:, i]\n    prob = y_prob[:, i]\n\n    best_threshold = 0.50\n    best_f1 = -1\n\n    # ----------------------------------------\n    # Try different thresholds\n    # ----------------------------------------\n\n    for threshold in thresholds:\n\n        pred = (prob >= threshold).astype(int)\n\n        f1 = f1_score(\n            true,\n            pred,\n            zero_division=0\n        )\n\n        if f1 > best_f1:\n            best_f1 = f1\n            best_threshold = threshold\n\n    # ----------------------------------------\n    # Predictions using best threshold\n    # ----------------------------------------\n\n    best_pred = (\n        prob >= best_threshold\n    ).astype(int)\n\n    accuracy = accuracy_score(\n        true,\n        best_pred\n    )\n\n    precision = precision_score(\n        true,\n        best_pred,\n        zero_division=0\n    )\n\n    recall = recall_score(\n        true,\n        best_pred,\n        zero_division=0\n    )\n\n    f1 = f1_score(\n        true,\n        best_pred,\n        zero_division=0\n    )\n\n    threshold_results.append({\n        \"Abnormality\": col,\n        \"Best Threshold\": best_threshold,\n        \"Accuracy\": accuracy,\n        \"Precision\": precision,\n        \"Recall\": recall,\n        \"F1\": f1\n    })\n\n\n# ============================================\n# RESULTS TABLE\n# ============================================\n\nthreshold_df = pd.DataFrame(\n    threshold_results\n)\n\nprint()\nprint(\"=\" * 85)\n\nprint(\n    f\"{'Abnormality':20s} | \"\n    f\"{'Threshold':>9s} | \"\n    f\"{'Accuracy':>8s} | \"\n    f\"{'Precision':>9s} | \"\n    f\"{'Recall':>7s} | \"\n    f\"{'F1':>7s}\"\n)\n\nprint(\"=\" * 85)\n\nfor _, row in threshold_df.iterrows():\n\n    print(\n        f\"{row['Abnormality']:20s} | \"\n        f\"{row['Best Threshold']:9.2f} | \"\n        f\"{row['Accuracy']:8.3f} | \"\n        f\"{row['Precision']:9.3f} | \"\n        f\"{row['Recall']:7.3f} | \"\n        f\"{row['F1']:7.3f}\"\n    )\n\nprint(\"=\" * 85)\n\n\n# ============================================\n# SAVE RESULTS\n# ============================================\n\nthreshold_path = (\n    \"/kaggle/working/\"\n    \"best_threshold_results.csv\"\n)\n\nthreshold_df.to_csv(\n    threshold_path,\n    index=False\n)\n\nprint()\nprint(\"Threshold optimization completed!\")\n\nprint()\nprint(\"Saved at:\")\nprint(threshold_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:31:05.894452Z","iopub.execute_input":"2026-08-08T11:31:05.894772Z","iopub.status.idle":"2026-08-08T11:31:06.253451Z","shell.execute_reply.started":"2026-08-08T11:31:05.894752Z","shell.execute_reply":"2026-08-08T11:31:06.252445Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 15 — FINAL PREDICTIONS USING OPTIMIZED THRESHOLDS\n# ============================================================\n\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\n\nprint(\"=\" * 60)\nprint(\"STEP 15 — FINAL PREDICTIONS\")\nprint(\"=\" * 60)\n\n# ------------------------------------------------------------\n# 1. Check required variables\n# ------------------------------------------------------------\n\nrequired = [\n    \"model\",\n    \"val_loader\",\n    \"label_cols\",\n    \"device\"\n]\n\nfor var in required:\n    if var not in globals():\n        raise NameError(\n            f\"{var} موجود ناهي. مٿي وارو code cell ٻيهر run ڪريو.\"\n        )\n\nprint(\"Required variables موجود آهن.\")\n\n# ------------------------------------------------------------\n# 2. Load best model\n# ------------------------------------------------------------\n\nmodel_path = \"/kaggle/working/best_knee_resnet18.pth\"\n\nif not os.path.exists(model_path):\n    raise FileNotFoundError(\n        f\"Model file نه مليو: {model_path}\"\n    )\n\ncheckpoint = torch.load(\n    model_path,\n    map_location=device\n)\n\n# جيڪڏهن checkpoint state_dict هجي\nif isinstance(checkpoint, dict) and \"model_state_dict\" in checkpoint:\n    model.load_state_dict(checkpoint[\"model_state_dict\"])\nelse:\n    model.load_state_dict(checkpoint)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Best model loaded successfully!\")\nprint(\"Model:\", model_path)\n\n# ------------------------------------------------------------\n# 3. Load optimized thresholds\n# ------------------------------------------------------------\n\nthreshold_path = \"/kaggle/working/best_threshold_results.csv\"\n\nif not os.path.exists(threshold_path):\n    raise FileNotFoundError(\n        f\"Threshold file نه مليو: {threshold_path}\"\n    )\n\nthreshold_df = pd.read_csv(threshold_path)\n\nprint(\"\\nThresholds loaded:\")\nprint(threshold_df)\n\n# Threshold column automatically find\nif \"Threshold\" in threshold_df.columns:\n    threshold_col = \"Threshold\"\nelif \"threshold\" in threshold_df.columns:\n    threshold_col = \"threshold\"\nelse:\n    raise KeyError(\n        \"Threshold column CSV ۾ نه مليو.\"\n    )\n\n# Abnormality column find\nif \"Abnormality\" in threshold_df.columns:\n    abnormality_col = \"Abnormality\"\nelif \"abnormality\" in threshold_df.columns:\n    abnormality_col = \"abnormality\"\nelse:\n    raise KeyError(\n        \"Abnormality column CSV ۾ نه مليو.\"\n    )\n\n# Dictionary\nthresholds = {}\n\nfor _, row in threshold_df.iterrows():\n    thresholds[row[abnormality_col]] = float(row[threshold_col])\n\nprint(\"\\nOptimized thresholds:\")\nfor label in label_cols:\n    print(\n        f\"{label:20s}: \"\n        f\"{thresholds.get(label, 0.50):.2f}\"\n    )\n\n# ------------------------------------------------------------\n# 4. Generate predictions\n# ------------------------------------------------------------\n\nall_probabilities = []\nall_true = []\n\nwith torch.no_grad():\n\n    for images, labels in val_loader:\n\n        images = images.to(device)\n        labels = labels.to(device)\n\n        # [B, 8, 3, 224, 224]\n        batch_size = images.shape[0]\n        num_images = images.shape[1]\n\n        # Reshape:\n        # [B,8,3,224,224]\n        # ->\n        # [B*8,3,224,224]\n\n        images_flat = images.view(\n            batch_size * num_images,\n            3,\n            224,\n            224\n        )\n\n        outputs = model(images_flat)\n\n        # [B*8,12]\n        outputs = outputs.view(\n            batch_size,\n            num_images,\n            len(label_cols)\n        )\n\n        # Average 8 MRI images\n        outputs = outputs.mean(dim=1)\n\n        probabilities = torch.sigmoid(outputs)\n\n        all_probabilities.append(\n            probabilities.cpu().numpy()\n        )\n\n        all_true.append(\n            labels.cpu().numpy()\n        )\n\n# Combine batches\nprobabilities = np.vstack(all_probabilities)\ntrue_labels = np.vstack(all_true)\n\nprint(\"\\nPrediction shape:\", probabilities.shape)\nprint(\"True labels shape:\", true_labels.shape)\n\n# ------------------------------------------------------------\n# 5. Apply optimized thresholds\n# ------------------------------------------------------------\n\nfinal_predictions = np.zeros_like(probabilities)\n\nfor i, label in enumerate(label_cols):\n\n    threshold = thresholds.get(label, 0.50)\n\n    final_predictions[:, i] = (\n        probabilities[:, i] >= threshold\n    ).astype(int)\n\n# ------------------------------------------------------------\n# 6. Create final DataFrame\n# ------------------------------------------------------------\n\nresults = []\n\nfor i in range(len(probabilities)):\n\n    row = {}\n\n    for j, label in enumerate(label_cols):\n\n        row[label + \"_true\"] = int(\n            true_labels[i, j]\n        )\n\n        row[label + \"_prob\"] = float(\n            probabilities[i, j]\n        )\n\n        row[label + \"_pred\"] = int(\n            final_predictions[i, j]\n        )\n\n    results.append(row)\n\nfinal_results = pd.DataFrame(results)\n\n# ------------------------------------------------------------\n# 7. Save results\n# ------------------------------------------------------------\n\noutput_path = \"/kaggle/working/final_validation_predictions.csv\"\n\nfinal_results.to_csv(\n    output_path,\n    index=False\n)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"STEP 15 COMPLETED\")\nprint(\"=\" * 60)\n\nprint(\"Final prediction shape:\")\nprint(final_predictions.shape)\n\nprint(\"\\nSaved at:\")\nprint(output_path)\n\n# ------------------------------------------------------------\n# 8. Show first 5 predictions\n# ------------------------------------------------------------\n\nprint(\"\\nFIRST 5 PREDICTIONS\")\nprint(\"=\" * 60)\n\ndisplay(final_results.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:32:21.161967Z","iopub.execute_input":"2026-08-08T11:32:21.162346Z","iopub.status.idle":"2026-08-08T11:32:21.224997Z","shell.execute_reply.started":"2026-08-08T11:32:21.162317Z","shell.execute_reply":"2026-08-08T11:32:21.223978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 15 — FINAL PREDICTIONS USING OPTIMIZED THRESHOLDS\n# FIXED VERSION\n# ============================================================\n\nimport os\nimport pandas as pd\nimport numpy as np\nimport torch\n\nprint(\"=\" * 60)\nprint(\"STEP 15 — FINAL PREDICTIONS\")\nprint(\"=\" * 60)\n\n# ------------------------------------------------------------\n# 1. Check required variables\n# ------------------------------------------------------------\n\nif \"model\" not in globals():\n    raise NameError(\"model موجود ناهي. Model وارو cell ٻيهر run ڪريو.\")\n\nif \"val_loader\" not in globals():\n    raise NameError(\"val_loader موجود ناهي. DataLoader وارو cell ٻيهر run ڪريو.\")\n\nif \"label_cols\" not in globals():\n    raise NameError(\"label_cols موجود ناهي.\")\n\nif \"device\" not in globals():\n    device = torch.device(\"cpu\")\n\nprint(\"Required variables OK\")\n\n# ------------------------------------------------------------\n# 2. Load best model\n# ------------------------------------------------------------\n\nmodel_path = \"/kaggle/working/best_knee_resnet18.pth\"\n\nif not os.path.exists(model_path):\n    raise FileNotFoundError(\n        f\"Model file نه مليو:\\n{model_path}\"\n    )\n\ncheckpoint = torch.load(\n    model_path,\n    map_location=device\n)\n\nif isinstance(checkpoint, dict) and \"model_state_dict\" in checkpoint:\n    model.load_state_dict(checkpoint[\"model_state_dict\"])\nelse:\n    model.load_state_dict(checkpoint)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Best model loaded successfully!\")\n\n# ------------------------------------------------------------\n# 3. Load threshold CSV\n# ------------------------------------------------------------\n\nthreshold_path = \"/kaggle/working/best_threshold_results.csv\"\n\nif not os.path.exists(threshold_path):\n    raise FileNotFoundError(\n        f\"Threshold file نه مليو:\\n{threshold_path}\"\n    )\n\nthreshold_df = pd.read_csv(threshold_path)\n\nprint(\"\\nThreshold CSV columns:\")\nprint(list(threshold_df.columns))\n\nprint(\"\\nFirst rows:\")\ndisplay(threshold_df.head())\n\n# ------------------------------------------------------------\n# 4. Automatically detect columns\n# ------------------------------------------------------------\n\ncolumns_lower = {\n    str(col).strip().lower(): col\n    for col in threshold_df.columns\n}\n\nprint(\"\\nDetected columns:\")\nprint(columns_lower)\n\n# Find abnormality column\nabnormality_col = None\n\nfor key, original in columns_lower.items():\n\n    if key in [\n        \"abnormality\",\n        \"label\",\n        \"class\",\n        \"name\"\n    ]:\n        abnormality_col = original\n        break\n\n# Find threshold column\nthreshold_col = None\n\nfor key, original in columns_lower.items():\n\n    if (\n        \"threshold\" in key\n        or key in [\"best_thresh\", \"best_threshold\"]\n    ):\n        threshold_col = original\n        break\n\n# ------------------------------------------------------------\n# 5. If threshold column not found, inspect numeric columns\n# ------------------------------------------------------------\n\nif abnormality_col is None:\n\n    # Usually first text column\n    for col in threshold_df.columns:\n\n        if threshold_df[col].dtype == \"object\":\n            abnormality_col = col\n            break\n\nif threshold_col is None:\n\n    # Find numeric column containing values between 0 and 1\n    for col in threshold_df.columns:\n\n        numeric_values = pd.to_numeric(\n            threshold_df[col],\n            errors=\"coerce\"\n        )\n\n        if numeric_values.notna().sum() == len(threshold_df):\n\n            if (\n                numeric_values.min() >= 0\n                and numeric_values.max() <= 1\n            ):\n                threshold_col = col\n                break\n\n# ------------------------------------------------------------\n# 6. Show detected columns\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"THRESHOLD COLUMN DETECTION\")\nprint(\"=\" * 60)\n\nprint(\"Abnormality column:\", abnormality_col)\nprint(\"Threshold column:\", threshold_col)\n\nif abnormality_col is None or threshold_col is None:\n\n    print(\"\\nERROR: Columns automatically detect نه ٿيا.\")\n\n    print(\"\\nCSV columns:\")\n    for col in threshold_df.columns:\n        print(\" -\", col)\n\n    raise KeyError(\n        \"Threshold/Abnormality column detect نه ٿي سگهيو.\"\n    )\n\n# ------------------------------------------------------------\n# 7. Create threshold dictionary\n# ------------------------------------------------------------\n\nthresholds = {}\n\nfor _, row in threshold_df.iterrows():\n\n    label = str(\n        row[abnormality_col]\n    ).strip()\n\n    value = pd.to_numeric(\n        row[threshold_col],\n        errors=\"coerce\"\n    )\n\n    if pd.notna(value):\n\n        thresholds[label] = float(value)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"OPTIMIZED THRESHOLDS\")\nprint(\"=\" * 60)\n\nfor label in label_cols:\n\n    threshold = thresholds.get(\n        label,\n        0.50\n    )\n\n    print(\n        f\"{label:20s}: {threshold:.2f}\"\n    )\n\n# ------------------------------------------------------------\n# 8. Generate validation predictions\n# ------------------------------------------------------------\n\nall_probabilities = []\nall_true = []\n\nwith torch.no_grad():\n\n    for images, labels in val_loader:\n\n        images = images.to(device)\n\n        # Shape:\n        # [B, 8, 3, 224, 224]\n\n        batch_size = images.shape[0]\n        num_images = images.shape[1]\n\n        images_flat = images.view(\n            batch_size * num_images,\n            3,\n            224,\n            224\n        )\n\n        outputs = model(images_flat)\n\n        # [B*8, 12]\n        outputs = outputs.view(\n            batch_size,\n            num_images,\n            len(label_cols)\n        )\n\n        # Average 8 MRI images\n        outputs = outputs.mean(dim=1)\n\n        probabilities_batch = torch.sigmoid(\n            outputs\n        )\n\n        all_probabilities.append(\n            probabilities_batch.cpu().numpy()\n        )\n\n        all_true.append(\n            labels.numpy()\n        )\n\n# ------------------------------------------------------------\n# 9. Combine results\n# ------------------------------------------------------------\n\nprobabilities = np.vstack(\n    all_probabilities\n)\n\ntrue_labels = np.vstack(\n    all_true\n)\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"PREDICTION RESULTS\")\nprint(\"=\" * 60)\n\nprint(\"Prediction shape:\", probabilities.shape)\nprint(\"True labels shape:\", true_labels.shape)\n\n# ------------------------------------------------------------\n# 10. Apply optimized thresholds\n# ------------------------------------------------------------\n\nfinal_predictions = np.zeros_like(\n    probabilities,\n    dtype=int\n)\n\nfor i, label in enumerate(label_cols):\n\n    threshold = thresholds.get(\n        label,\n        0.50\n    )\n\n    final_predictions[:, i] = (\n        probabilities[:, i] >= threshold\n    ).astype(int)\n\n# ------------------------------------------------------------\n# 11. Create results DataFrame\n# ------------------------------------------------------------\n\nresults = {}\n\nfor i, label in enumerate(label_cols):\n\n    results[label + \"_true\"] = (\n        true_labels[:, i].astype(int)\n    )\n\n    results[label + \"_prob\"] = (\n        probabilities[:, i]\n    )\n\n    results[label + \"_pred\"] = (\n        final_predictions[:, i]\n    )\n\nfinal_results = pd.DataFrame(\n    results\n)\n\n# ------------------------------------------------------------\n# 12. Save\n# ------------------------------------------------------------\n\noutput_path = (\n    \"/kaggle/working/\"\n    \"final_validation_predictions.csv\"\n)\n\nfinal_results.to_csv(\n    output_path,\n    index=False\n)\n\n# ------------------------------------------------------------\n# 13. Final output\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"STEP 15 COMPLETED SUCCESSFULLY\")\nprint(\"=\" * 60)\n\nprint(\n    \"Final prediction shape:\",\n    final_predictions.shape\n)\n\nprint(\n    \"Saved at:\",\n    output_path\n)\n\nprint(\"\\nFirst 5 results:\")\ndisplay(\n    final_results.head()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:33:18.224696Z","iopub.execute_input":"2026-08-08T11:33:18.225106Z","iopub.status.idle":"2026-08-08T11:33:21.558259Z","shell.execute_reply.started":"2026-08-08T11:33:18.225071Z","shell.execute_reply":"2026-08-08T11:33:21.556728Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 16 — FINAL MODEL PERFORMANCE\n# ============================================================\n\nimport os\nimport pandas as pd\nimport numpy as np\n\nfrom sklearn.metrics import (\n    accuracy_score,\n    precision_score,\n    recall_score,\n    f1_score,\n    roc_auc_score,\n    confusion_matrix\n)\n\nprint(\"=\" * 80)\nprint(\"STEP 16 — FINAL MODEL PERFORMANCE\")\nprint(\"=\" * 80)\n\n# ------------------------------------------------------------\n# 1. Load final predictions\n# ------------------------------------------------------------\n\nprediction_path = \"/kaggle/working/final_validation_predictions.csv\"\n\nif not os.path.exists(prediction_path):\n    raise FileNotFoundError(\n        f\"File نه مليو:\\n{prediction_path}\\n\"\n        \"پهريان Step 15 هلائو.\"\n    )\n\ndf = pd.read_csv(prediction_path)\n\nprint(\"Prediction file loaded successfully!\")\nprint(\"Rows:\", len(df))\nprint(\"Columns:\", len(df.columns))\n\n# ------------------------------------------------------------\n# 2. Label list\n# ------------------------------------------------------------\n\nlabel_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# 3. Calculate performance\n# ------------------------------------------------------------\n\nperformance = []\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"PERFORMANCE\")\nprint(\"=\" * 80)\n\nfor label in label_cols:\n\n    true_col = label + \"_true\"\n    prob_col = label + \"_prob\"\n    pred_col = label + \"_pred\"\n\n    y_true = df[true_col].astype(int)\n    y_prob = df[prob_col].astype(float)\n    y_pred = df[pred_col].astype(int)\n\n    accuracy = accuracy_score(\n        y_true,\n        y_pred\n    )\n\n    precision = precision_score(\n        y_true,\n        y_pred,\n        zero_division=0\n    )\n\n    recall = recall_score(\n        y_true,\n        y_pred,\n        zero_division=0\n    )\n\n    f1 = f1_score(\n        y_true,\n        y_pred,\n        zero_division=0\n    )\n\n    # AUC only possible if both classes exist\n    if y_true.nunique() == 2:\n\n        auc = roc_auc_score(\n            y_true,\n            y_prob\n        )\n\n    else:\n\n        auc = np.nan\n\n    performance.append({\n        \"Abnormality\": label,\n        \"AUC\": auc,\n        \"Accuracy\": accuracy,\n        \"Precision\": precision,\n        \"Recall\": recall,\n        \"F1\": f1\n    })\n\n# ------------------------------------------------------------\n# 4. Performance DataFrame\n# ------------------------------------------------------------\n\nperformance_df = pd.DataFrame(\n    performance\n)\n\nprint(\"\\n\")\nprint(\n    performance_df.to_string(\n        index=False,\n        formatters={\n            \"AUC\": lambda x:\n                \"N/A\" if pd.isna(x)\n                else f\"{x:.3f}\",\n\n            \"Accuracy\": lambda x:\n                f\"{x:.3f}\",\n\n            \"Precision\": lambda x:\n                f\"{x:.3f}\",\n\n            \"Recall\": lambda x:\n                f\"{x:.3f}\",\n\n            \"F1\": lambda x:\n                f\"{x:.3f}\"\n        }\n    )\n)\n\n# ------------------------------------------------------------\n# 5. Save performance\n# ------------------------------------------------------------\n\nperformance_path = (\n    \"/kaggle/working/\"\n    \"final_threshold_performance.csv\"\n)\n\nperformance_df.to_csv(\n    performance_path,\n    index=False\n)\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"PERFORMANCE SAVED\")\nprint(\"=\" * 80)\n\nprint(\n    performance_path\n)\n\n# ------------------------------------------------------------\n# 6. Confusion matrices\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"CONFUSION MATRICES\")\nprint(\"=\" * 80)\n\nfor label in label_cols:\n\n    y_true = df[\n        label + \"_true\"\n    ].astype(int)\n\n    y_pred = df[\n        label + \"_pred\"\n    ].astype(int)\n\n    cm = confusion_matrix(\n        y_true,\n        y_pred,\n        labels=[0, 1]\n    )\n\n    print(f\"\\n{label}\")\n    print(\"                Predicted\")\n    print(\"                 0    1\")\n    print(\n        f\"Actual 0     {cm[0,0]:4d} {cm[0,1]:4d}\"\n    )\n    print(\n        f\"Actual 1     {cm[1,0]:4d} {cm[1,1]:4d}\"\n    )\n\n# ------------------------------------------------------------\n# 7. Average performance\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"AVERAGE PERFORMANCE\")\nprint(\"=\" * 80)\n\nprint(\n    f\"Average Accuracy : \"\n    f\"{performance_df['Accuracy'].mean():.3f}\"\n)\n\nprint(\n    f\"Average Precision: \"\n    f\"{performance_df['Precision'].mean():.3f}\"\n)\n\nprint(\n    f\"Average Recall   : \"\n    f\"{performance_df['Recall'].mean():.3f}\"\n)\n\nprint(\n    f\"Average F1       : \"\n    f\"{performance_df['F1'].mean():.3f}\"\n)\n\nprint(\n    f\"Average AUC      : \"\n    f\"{performance_df['AUC'].mean():.3f}\"\n)\n\nprint(\"\\n\" + \"=\" * 80)\nprint(\"STEP 16 COMPLETED SUCCESSFULLY\")\nprint(\"=\" * 80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:34:39.259424Z","iopub.execute_input":"2026-08-08T11:34:39.259781Z","iopub.status.idle":"2026-08-08T11:34:39.38839Z","shell.execute_reply.started":"2026-08-08T11:34:39.259754Z","shell.execute_reply":"2026-08-08T11:34:39.387507Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 17 — FINAL PROJECT PACKAGE\n# ============================================================\n\nimport os\nimport shutil\nimport zipfile\nimport pandas as pd\n\nprint(\"=\" * 70)\nprint(\"STEP 17 — FINAL PROJECT PACKAGE\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# 1. Paths\n# ------------------------------------------------------------\n\nfiles = {\n    \"model\": \"/kaggle/working/best_knee_resnet18.pth\",\n    \"predictions\": \"/kaggle/working/final_validation_predictions.csv\",\n    \"performance\": \"/kaggle/working/final_threshold_performance.csv\",\n    \"thresholds\": \"/kaggle/working/best_threshold_results.csv\",\n}\n\n# ------------------------------------------------------------\n# 2. Check files\n# ------------------------------------------------------------\n\nprint(\"\\nChecking project files...\\n\")\n\nfor name, path in files.items():\n\n    if os.path.exists(path):\n\n        size_mb = os.path.getsize(path) / (1024 * 1024)\n\n        print(\n            f\"✓ {name:15s} | \"\n            f\"{size_mb:.2f} MB | \"\n            f\"{path}\"\n        )\n\n    else:\n\n        print(\n            f\"✗ {name:15s} | NOT FOUND\"\n        )\n\n# ------------------------------------------------------------\n# 3. Create project folder\n# ------------------------------------------------------------\n\nproject_dir = \"/kaggle/working/Knee_MRI_Abnormality_Project\"\n\nif os.path.exists(project_dir):\n    shutil.rmtree(project_dir)\n\nos.makedirs(project_dir)\n\n# ------------------------------------------------------------\n# 4. Copy available files\n# ------------------------------------------------------------\n\nfor name, path in files.items():\n\n    if os.path.exists(path):\n\n        destination = os.path.join(\n            project_dir,\n            os.path.basename(path)\n        )\n\n        shutil.copy2(\n            path,\n            destination\n        )\n\n# ------------------------------------------------------------\n# 5. Create project information\n# ------------------------------------------------------------\n\ninfo = \"\"\"\nKNEE MRI ABNORMALITY DETECTION\n================================\n\nProject Type:\nDeep Learning / Medical Image Classification\n\nModel:\nResNet18\n\nTask:\nMulti-label classification of knee MRI abnormalities.\n\nNumber of Classes:\n12\n\nClasses:\n1. ACL\n2. MCL\n3. Medial Meniscus\n4. Lateral Meniscus\n5. Medial OA\n6. Lateral OA\n7. PF OA\n8. Effusion\n9. Synovitis\n10. Baker's\n11. Contusion\n12. Fracture\n\nDataset:\nRSNA Knee MRI Abnormality Detection\n\nLabeled Studies:\n58\n\nTraining Studies:\n46\n\nValidation Studies:\n12\n\nMRI Images per Study:\n8\n\nImage Size:\n224 x 224\n\nLoss:\nWeighted BCEWithLogitsLoss\n\nOptimizer:\nAdamW\n\nLearning Rate:\n0.00005\n\nWeight Decay:\n0.01\n\nBest Model:\nbest_knee_resnet18.pth\n\nIMPORTANT:\nThis project is a research/prototype system.\nThe reported validation performance is based on a small\nvalidation set and should NOT be interpreted as a clinical\ndiagnostic system.\n\"\"\"\n\nwith open(\n    os.path.join(project_dir, \"PROJECT_INFO.txt\"),\n    \"w\"\n) as f:\n    f.write(info)\n\n# ------------------------------------------------------------\n# 6. Create README\n# ------------------------------------------------------------\n\nreadme = \"\"\"\n# Knee MRI Abnormality Detection\n\nA deep learning prototype for multi-label classification\nof abnormalities in knee MRI studies.\n\n## Model\n\nResNet18\n\n## Abnormalities\n\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## Dataset\n\n58 labeled knee MRI studies were used.\n\nEach study contains 8 MRI images.\n\n## Training\n\nLoss:\nWeighted BCEWithLogitsLoss\n\nOptimizer:\nAdamW\n\nLearning Rate:\n0.00005\n\n## Important Limitation\n\nThe current dataset is small and the validation set contains\nonly 12 studies. Therefore this model is intended as a\nresearch/educational prototype and not as a clinical\ndiagnostic tool.\n\n## Files\n\nbest_knee_resnet18.pth\nfinal_validation_predictions.csv\nfinal_threshold_performance.csv\nbest_threshold_results.csv\nPROJECT_INFO.txt\n\"\"\"\n\nwith open(\n    os.path.join(project_dir, \"README.md\"),\n    \"w\"\n) as f:\n    f.write(readme)\n\n# ------------------------------------------------------------\n# 7. Create ZIP\n# ------------------------------------------------------------\n\nzip_path = \"/kaggle/working/Knee_MRI_Abnormality_Project.zip\"\n\nif os.path.exists(zip_path):\n    os.remove(zip_path)\n\nwith zipfile.ZipFile(\n    zip_path,\n    \"w\",\n    zipfile.ZIP_DEFLATED\n) as zipf:\n\n    for root, dirs, filenames in os.walk(\n        project_dir\n    ):\n\n        for filename in filenames:\n\n            full_path = os.path.join(\n                root,\n                filename\n            )\n\n            arcname = os.path.relpath(\n                full_path,\n                project_dir\n            )\n\n            zipf.write(\n                full_path,\n                arcname\n            )\n\n# ------------------------------------------------------------\n# 8. Final report\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"PROJECT PACKAGE CREATED\")\nprint(\"=\" * 70)\n\nprint(\"Folder:\")\nprint(project_dir)\n\nprint(\"\\nZIP:\")\nprint(zip_path)\n\nprint(\n    \"\\nZIP size:\",\n    round(\n        os.path.getsize(zip_path) / (1024 * 1024),\n        2\n    ),\n    \"MB\"\n)\n\nprint(\"\\nFiles inside package:\")\n\nfor filename in sorted(\n    os.listdir(project_dir)\n):\n    print(\"✓\", filename)\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"STEP 17 COMPLETED\")\nprint(\"=\" * 70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:37:37.690882Z","iopub.execute_input":"2026-08-08T11:37:37.691176Z","iopub.status.idle":"2026-08-08T11:37:39.592177Z","shell.execute_reply.started":"2026-08-08T11:37:37.691155Z","shell.execute_reply":"2026-08-08T11:37:39.591369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 — FINAL PROJECT REPORT (PDF)\n# ============================================================\n\nimport os\nimport pandas as pd\n\nfrom reportlab.lib import colors\nfrom reportlab.lib.pagesizes import A4\nfrom reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle\nfrom reportlab.lib.enums import TA_CENTER\nfrom reportlab.platypus import (\n    SimpleDocTemplate,\n    Paragraph,\n    Spacer,\n    Table,\n    TableStyle,\n    PageBreak\n)\n\nprint(\"=\" * 70)\nprint(\"STEP 18 — FINAL PROJECT REPORT\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# PATHS\n# ------------------------------------------------------------\n\nproject_dir = \"/kaggle/working/Knee_MRI_Abnormality_Project\"\n\nos.makedirs(project_dir, exist_ok=True)\n\nperformance_path = (\n    \"/kaggle/working/final_threshold_performance.csv\"\n)\n\n# ------------------------------------------------------------\n# LOAD PERFORMANCE\n# ------------------------------------------------------------\n\nperformance_df = pd.read_csv(\n    performance_path\n)\n\n# ------------------------------------------------------------\n# PDF PATH\n# ------------------------------------------------------------\n\npdf_path = os.path.join(\n    project_dir,\n    \"Knee_MRI_Abnormality_Detection_Report.pdf\"\n)\n\n# ------------------------------------------------------------\n# DOCUMENT\n# ------------------------------------------------------------\n\ndoc = SimpleDocTemplate(\n    pdf_path,\n    pagesize=A4,\n    rightMargin=45,\n    leftMargin=45,\n    topMargin=45,\n    bottomMargin=45\n)\n\nstyles = getSampleStyleSheet()\n\ntitle_style = ParagraphStyle(\n    \"TitleCustom\",\n    parent=styles[\"Title\"],\n    alignment=TA_CENTER,\n    fontSize=20,\n    leading=25,\n    spaceAfter=20\n)\n\nheading_style = ParagraphStyle(\n    \"HeadingCustom\",\n    parent=styles[\"Heading2\"],\n    fontSize=14,\n    leading=18,\n    spaceBefore=12,\n    spaceAfter=8\n)\n\nbody_style = ParagraphStyle(\n    \"BodyCustom\",\n    parent=styles[\"BodyText\"],\n    fontSize=10,\n    leading=15,\n    spaceAfter=8\n)\n\nsmall_style = ParagraphStyle(\n    \"SmallCustom\",\n    parent=styles[\"BodyText\"],\n    fontSize=8,\n    leading=10\n)\n\nstory = []\n\n# ------------------------------------------------------------\n# TITLE\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"Knee MRI Abnormality Detection\",\n        title_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"<b>Deep Learning Based Multi-Label Classification \"\n        \"of Knee MRI Abnormalities</b>\",\n        body_style\n    )\n)\n\nstory.append(Spacer(1, 15))\n\nstory.append(\n    Paragraph(\n        \"Final Project Report\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"Model: ResNet18 | Framework: PyTorch | \"\n        \"Task: Multi-Label MRI Abnormality Classification\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 1. ABSTRACT\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"1. Abstract\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"This project presents a deep learning prototype for \"\n        \"automatically detecting multiple abnormalities in knee \"\n        \"magnetic resonance imaging (MRI) studies. A ResNet18 \"\n        \"convolutional neural network was used for multi-label \"\n        \"classification of twelve knee abnormalities. Each study \"\n        \"was represented using eight MRI images resized to \"\n        \"224 × 224 pixels. Weighted Binary Cross Entropy with \"\n        \"Logits Loss was used to address class imbalance, while \"\n        \"AdamW was used as the optimizer. The model was evaluated \"\n        \"on a validation set of twelve studies. The results \"\n        \"demonstrate the feasibility of the approach while also \"\n        \"highlighting the limitations caused by the small labeled \"\n        \"dataset and limited validation set.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 2. INTRODUCTION\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"2. Introduction\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"Knee MRI provides detailed information about soft tissues, \"\n        \"ligaments, menisci, cartilage, bone and joint structures. \"\n        \"Manual interpretation requires specialized expertise and \"\n        \"can be time-consuming. Deep learning can assist researchers \"\n        \"in developing automated image analysis systems.\"\n        \"<br/><br/>\"\n        \"The objective of this project is to develop a prototype \"\n        \"that predicts multiple abnormalities from knee MRI studies. \"\n        \"Because several abnormalities can occur in the same study, \"\n        \"the task is formulated as a multi-label classification \"\n        \"problem.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 3. OBJECTIVE\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"3. Project Objectives\",\n        heading_style\n    )\n)\n\nobjectives = [\n    \"Process knee MRI DICOM images.\",\n    \"Represent each MRI study using multiple images.\",\n    \"Train a ResNet18 deep learning model.\",\n    \"Predict twelve knee abnormalities simultaneously.\",\n    \"Address class imbalance using weighted loss.\",\n    \"Evaluate the model using AUC, accuracy, precision, recall and F1-score.\"\n]\n\nfor item in objectives:\n    story.append(\n        Paragraph(\n            \"• \" + item,\n            body_style\n        )\n    )\n\n# ------------------------------------------------------------\n# 4. DATASET\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"4. Dataset\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"The project used the RSNA Knee MRI Abnormality Detection \"\n        \"dataset available in the Kaggle environment. The dataset \"\n        \"contained 4,407 MRI series folders and more than 10,000 \"\n        \"DICOM images. After matching the available labels with MRI \"\n        \"studies, 58 studies contained complete labels for the \"\n        \"twelve target abnormalities.\",\n        body_style\n    )\n)\n\ndataset_table = [\n    [\"Property\", \"Value\"],\n    [\"Labeled studies\", \"58\"],\n    [\"Training studies\", \"46\"],\n    [\"Validation studies\", \"12\"],\n    [\"MRI images per study\", \"8\"],\n    [\"Image size\", \"224 × 224\"],\n    [\"Number of classes\", \"12\"]\n]\n\ntable = Table(\n    dataset_table,\n    colWidths=[220, 180]\n)\n\ntable.setStyle(\n    TableStyle([\n        (\"BACKGROUND\", (0,0), (-1,0), colors.lightgrey),\n        (\"GRID\", (0,0), (-1,-1), 0.5, colors.grey),\n        (\"FONTNAME\", (0,0), (-1,0), \"Helvetica-Bold\"),\n        (\"FONTSIZE\", (0,0), (-1,-1), 9),\n        (\"VALIGN\", (0,0), (-1,-1), \"MIDDLE\"),\n    ])\n)\n\nstory.append(table)\n\n# ------------------------------------------------------------\n# 5. CLASSES\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"5. Target Abnormalities\",\n        heading_style\n    )\n)\n\nclasses = [\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\nstory.append(\n    Paragraph(\n        \", \".join(classes) + \".\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 6. PREPROCESSING\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"6. Image Preprocessing\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"DICOM MRI images were loaded using pydicom. Eight images \"\n        \"were selected for each study and converted into image \"\n        \"tensors. Images were resized to 224 × 224 pixels and \"\n        \"converted to three-channel input suitable for ResNet18. \"\n        \"Study-level predictions were obtained by aggregating \"\n        \"predictions across the selected MRI images.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 7. MODEL\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"7. Deep Learning Model\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"ResNet18 was selected as the backbone convolutional neural \"\n        \"network. The original final classification layer was \"\n        \"replaced with a fully connected layer containing twelve \"\n        \"outputs, one for each abnormality. Since this is a \"\n        \"multi-label problem, sigmoid probabilities were generated \"\n        \"for the twelve output classes.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 8. TRAINING\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"8. Training Configuration\",\n        heading_style\n    )\n)\n\ntraining_table = [\n    [\"Parameter\", \"Configuration\"],\n    [\"Architecture\", \"ResNet18\"],\n    [\"Loss\", \"Weighted BCEWithLogitsLoss\"],\n    [\"Optimizer\", \"AdamW\"],\n    [\"Learning rate\", \"0.00005\"],\n    [\"Weight decay\", \"0.01\"],\n    [\"Training studies\", \"46\"],\n    [\"Validation studies\", \"12\"],\n    [\"Output classes\", \"12\"]\n]\n\ntable = Table(\n    training_table,\n    colWidths=[220, 180]\n)\n\ntable.setStyle(\n    TableStyle([\n        (\"BACKGROUND\", (0,0), (-1,0), colors.lightgrey),\n        (\"GRID\", (0,0), (-1,-1), 0.5, colors.grey),\n        (\"FONTNAME\", (0,0), (-1,0), \"Helvetica-Bold\"),\n        (\"FONTSIZE\", (0,0), (-1,-1), 9),\n    ])\n)\n\nstory.append(table)\n\n# ------------------------------------------------------------\n# 9. RESULTS\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"9. Model Performance\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"The final validation results obtained after threshold \"\n        \"optimization are shown below.\",\n        body_style\n    )\n)\n\nresult_data = [\n    [\n        \"Abnormality\",\n        \"AUC\",\n        \"Accuracy\",\n        \"Precision\",\n        \"Recall\",\n        \"F1\"\n    ]\n]\n\nfor _, row in performance_df.iterrows():\n\n    auc = (\n        \"N/A\"\n        if pd.isna(row[\"AUC\"])\n        else f\"{row['AUC']:.3f}\"\n    )\n\n    result_data.append([\n        row[\"Abnormality\"],\n        auc,\n        f\"{row['Accuracy']:.3f}\",\n        f\"{row['Precision']:.3f}\",\n        f\"{row['Recall']:.3f}\",\n        f\"{row['F1']:.3f}\"\n    ])\n\ntable = Table(\n    result_data,\n    colWidths=[\n        125,\n        55,\n        65,\n        65,\n        55,\n        50\n    ],\n    repeatRows=1\n)\n\ntable.setStyle(\n    TableStyle([\n        (\"BACKGROUND\", (0,0), (-1,0), colors.lightgrey),\n        (\"GRID\", (0,0), (-1,-1), 0.4, colors.grey),\n        (\"FONTNAME\", (0,0), (-1,0), \"Helvetica-Bold\"),\n        (\"FONTSIZE\", (0,0), (-1,-1), 7),\n        (\"VALIGN\", (0,0), (-1,-1), \"MIDDLE\"),\n    ])\n)\n\nstory.append(table)\n\n# ------------------------------------------------------------\n# 10. OVERALL RESULTS\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"10. Overall Results\",\n        heading_style\n    )\n)\n\navg_accuracy = performance_df[\"Accuracy\"].mean()\navg_precision = performance_df[\"Precision\"].mean()\navg_recall = performance_df[\"Recall\"].mean()\navg_f1 = performance_df[\"F1\"].mean()\navg_auc = performance_df[\"AUC\"].mean()\n\noverall_table = [\n    [\"Metric\", \"Average\"],\n    [\"Accuracy\", f\"{avg_accuracy:.3f}\"],\n    [\"Precision\", f\"{avg_precision:.3f}\"],\n    [\"Recall\", f\"{avg_recall:.3f}\"],\n    [\"F1-score\", f\"{avg_f1:.3f}\"],\n    [\"AUC\", f\"{avg_auc:.3f}\"]\n]\n\ntable = Table(\n    overall_table,\n    colWidths=[220, 180]\n)\n\ntable.setStyle(\n    TableStyle([\n        (\"BACKGROUND\", (0,0), (-1,0), colors.lightgrey),\n        (\"GRID\", (0,0), (-1,-1), 0.5, colors.grey),\n        (\"FONTNAME\", (0,0), (-1,0), \"Helvetica-Bold\"),\n        (\"FONTSIZE\", (0,0), (-1,-1), 9),\n    ])\n)\n\nstory.append(table)\n\n# ------------------------------------------------------------\n# 11. DISCUSSION\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"11. Discussion\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"The results show that performance varies substantially \"\n        \"between abnormalities. Medial OA and Baker's demonstrated \"\n        \"relatively stronger discrimination in the current validation \"\n        \"set, while Contusion and PF OA showed weaker performance. \"\n        \"Several classes also had insufficient positive examples in \"\n        \"the validation set, making their AUC unavailable or unstable.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 12. LIMITATIONS\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"12. Limitations\",\n        heading_style\n    )\n)\n\nlimitations = [\n    \"Only 58 studies had complete labels for all twelve abnormalities.\",\n    \"The validation set contained only 12 studies.\",\n    \"Some abnormalities had very few positive examples.\",\n    \"A small validation set produces unstable performance estimates.\",\n    \"The model has not been clinically validated.\",\n    \"The system should not be used as a standalone diagnostic tool.\"\n]\n\nfor item in limitations:\n    story.append(\n        Paragraph(\n            \"• \" + item,\n            body_style\n        )\n    )\n\n# ------------------------------------------------------------\n# 13. CONCLUSION\n# ------------------------------------------------------------\n\nstory.append(\n    Paragraph(\n        \"13. Conclusion\",\n        heading_style\n    )\n)\n\nstory.append(\n    Paragraph(\n        \"A ResNet18-based multi-label deep learning prototype was \"\n        \"successfully developed for knee MRI abnormality detection. \"\n        \"The complete pipeline includes DICOM processing, study-level \"\n        \"image preparation, weighted multi-label training, probability \"\n        \"prediction, threshold optimization and performance \"\n        \"evaluation. The project demonstrates the feasibility of \"\n        \"automated knee MRI analysis, while the current results \"\n        \"indicate that a substantially larger labeled dataset and \"\n        \"more rigorous validation are required before clinical use.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# 14. FINAL NOTE\n# ------------------------------------------------------------\n\nstory.append(\n    Spacer(1, 15)\n)\n\nstory.append(\n    Paragraph(\n        \"<b>Important:</b> This project is a research and educational \"\n        \"prototype. The model predictions must not be interpreted as \"\n        \"medical diagnoses.\",\n        body_style\n    )\n)\n\n# ------------------------------------------------------------\n# BUILD PDF\n# ------------------------------------------------------------\n\ndoc.build(story)\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"STEP 18 COMPLETED SUCCESSFULLY\")\nprint(\"=\" * 70)\n\nprint(\"\\nReport created:\")\nprint(pdf_path)\n\nprint(\n    \"\\nReport size:\",\n    round(\n        os.path.getsize(pdf_path) / 1024,\n        2\n    ),\n    \"KB\"\n)\n\nprint(\"\\nProject folder now contains:\")\n\nfor file in sorted(\n    os.listdir(project_dir)\n):\n    print(\"✓\", file)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:41:47.319256Z","iopub.execute_input":"2026-08-08T11:41:47.319631Z","iopub.status.idle":"2026-08-08T11:41:47.451987Z","shell.execute_reply.started":"2026-08-08T11:41:47.319602Z","shell.execute_reply":"2026-08-08T11:41:47.451148Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install reportlab -q","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:40:15.649451Z","iopub.execute_input":"2026-08-08T11:40:15.649728Z","iopub.status.idle":"2026-08-08T11:40:21.015691Z","shell.execute_reply.started":"2026-08-08T11:40:15.649706Z","shell.execute_reply":"2026-08-08T11:40:21.014249Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 19 — FINAL SUBMISSION PACKAGE\n# ============================================================\n\nimport os\nimport shutil\nimport zipfile\n\nprint(\"=\" * 70)\nprint(\"STEP 19 — FINAL SUBMISSION PACKAGE\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# Paths\n# ------------------------------------------------------------\n\nproject_dir = \"/kaggle/working/Knee_MRI_Abnormality_Project\"\n\nfinal_zip = (\n    \"/kaggle/working/\"\n    \"Knee_MRI_Abnormality_Project_FINAL.zip\"\n)\n\n# ------------------------------------------------------------\n# Check project folder\n# ------------------------------------------------------------\n\nif not os.path.exists(project_dir):\n    raise FileNotFoundError(\n        \"Project folder نه مليو.\"\n    )\n\n# ------------------------------------------------------------\n# Required files\n# ------------------------------------------------------------\n\nrequired_files = [\n    \"best_knee_resnet18.pth\",\n    \"final_validation_predictions.csv\",\n    \"final_threshold_performance.csv\",\n    \"best_threshold_results.csv\",\n    \"README.md\",\n    \"PROJECT_INFO.txt\",\n    \"Knee_MRI_Abnormality_Detection_Report.pdf\"\n]\n\nprint(\"\\nChecking final project files...\\n\")\n\nmissing = []\n\nfor filename in required_files:\n\n    path = os.path.join(\n        project_dir,\n        filename\n    )\n\n    if os.path.exists(path):\n\n        size = os.path.getsize(path)\n\n        print(\n            f\"✓ {filename:45s} \"\n            f\"{size / 1024:.2f} KB\"\n        )\n\n    else:\n\n        print(\n            f\"✗ MISSING: {filename}\"\n        )\n\n        missing.append(filename)\n\n# ------------------------------------------------------------\n# Stop if files missing\n# ------------------------------------------------------------\n\nif missing:\n\n    raise FileNotFoundError(\n        \"These files are missing:\\n\"\n        + \"\\n\".join(missing)\n    )\n\n# ------------------------------------------------------------\n# Remove old final ZIP\n# ------------------------------------------------------------\n\nif os.path.exists(final_zip):\n    os.remove(final_zip)\n\n# ------------------------------------------------------------\n# Create FINAL ZIP\n# ------------------------------------------------------------\n\nwith zipfile.ZipFile(\n    final_zip,\n    \"w\",\n    zipfile.ZIP_DEFLATED\n) as zipf:\n\n    for root, dirs, files in os.walk(\n        project_dir\n    ):\n\n        for file in files:\n\n            full_path = os.path.join(\n                root,\n                file\n            )\n\n            arcname = os.path.relpath(\n                full_path,\n                project_dir\n            )\n\n            zipf.write(\n                full_path,\n                arcname\n            )\n\n# ------------------------------------------------------------\n# Verify ZIP\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"FINAL ZIP CREATED\")\nprint(\"=\" * 70)\n\nprint(\n    \"\\nFile:\",\n    final_zip\n)\n\nzip_size_mb = (\n    os.path.getsize(final_zip)\n    / (1024 * 1024)\n)\n\nprint(\n    \"ZIP size:\",\n    f\"{zip_size_mb:.2f} MB\"\n)\n\n# ------------------------------------------------------------\n# List ZIP contents\n# ------------------------------------------------------------\n\nprint(\"\\nFiles inside FINAL ZIP:\\n\")\n\nwith zipfile.ZipFile(\n    final_zip,\n    \"r\"\n) as zipf:\n\n    for name in zipf.namelist():\n        print(\"✓\", name)\n\n# ------------------------------------------------------------\n# Final success\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"STEP 19 COMPLETED SUCCESSFULLY\")\nprint(\"=\" * 70)\n\nprint(\"\\nFINAL SUBMISSION FILE:\")\nprint(final_zip)\n\nprint(\"\\nPROJECT STATUS:\")\nprint(\"✓ Model trained\")\nprint(\"✓ Best model saved\")\nprint(\"✓ Predictions generated\")\nprint(\"✓ Threshold optimization completed\")\nprint(\"✓ Performance evaluated\")\nprint(\"✓ Project README created\")\nprint(\"✓ Final PDF report created\")\nprint(\"✓ Final ZIP package created\")\n\nprint(\"\\nREADY FOR SUBMISSION!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:43:39.122129Z","iopub.execute_input":"2026-08-08T11:43:39.122479Z","iopub.status.idle":"2026-08-08T11:43:41.028527Z","shell.execute_reply.started":"2026-08-08T11:43:39.122458Z","shell.execute_reply":"2026-08-08T11:43:41.027458Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from IPython.display import FileLink, display\n\nfile_path = \"/kaggle/working/Knee_MRI_Abnormality_Project_FINAL.zip\"\n\ndisplay(FileLink(file_path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:54:44.908556Z","iopub.execute_input":"2026-08-08T11:54:44.909898Z","iopub.status.idle":"2026-08-08T11:54:44.91596Z","shell.execute_reply.started":"2026-08-08T11:54:44.90978Z","shell.execute_reply":"2026-08-08T11:54:44.915201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nfrom IPython.display import display, FileLink\n\nsrc = \"/kaggle/working/Knee_MRI_Abnormality_Project_FINAL.zip\"\ndst = \"/kaggle/working/final_project.zip\"\n\nshutil.copy2(src, dst)\n\nprint(\"File ready:\")\nprint(dst)\n\ndisplay(FileLink(\"final_project.zip\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T11:56:48.467315Z","iopub.execute_input":"2026-08-08T11:56:48.467686Z","iopub.status.idle":"2026-08-08T11:56:48.50925Z","shell.execute_reply.started":"2026-08-08T11:56:48.467652Z","shell.execute_reply":"2026-08-08T11:56:48.508227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nsample_path = \"/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv\"\n\nsample_submission = pd.read_csv(sample_path)\n\nprint(\"============================================\")\nprint(\"SAMPLE SUBMISSION\")\nprint(\"============================================\")\n\nprint(\"Shape:\", sample_submission.shape)\nprint(\"Columns:\")\nprint(sample_submission.columns.tolist())\n\ndisplay(sample_submission.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:44:03.568015Z","iopub.execute_input":"2026-08-08T19:44:03.568387Z","iopub.status.idle":"2026-08-08T19:44:03.956794Z","shell.execute_reply.started":"2026-08-08T19:44:03.568356Z","shell.execute_reply":"2026-08-08T19:44:03.955937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\n\nBASE = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\nprint(\"============================================\")\nprint(\"CHECKING TEST DATA\")\nprint(\"============================================\")\n\ntest_csv = os.path.join(BASE, \"test.csv\")\ntest_series_csv = os.path.join(BASE, \"test_series.csv\")\ntest_series = os.path.join(BASE, \"test_series\")\n\ntest_df = pd.read_csv(test_csv)\ntest_series_df = pd.read_csv(test_series_csv)\n\nprint(\"test.csv shape:\", test_df.shape)\nprint(\"test.csv columns:\")\nprint(test_df.columns.tolist())\n\nprint(\"\\ntest_series.csv shape:\", test_series_df.shape)\nprint(\"test_series.csv columns:\")\nprint(test_series_df.columns.tolist())\n\nprint(\"\\nTest MRI folder exists:\", os.path.exists(test_series))\n\nprint(\"\\nFirst test studies:\")\ndisplay(test_df.head())\n\nprint(\"\\nFirst test series:\")\ndisplay(test_series_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:45:27.64817Z","iopub.execute_input":"2026-08-08T19:45:27.64849Z","iopub.status.idle":"2026-08-08T19:45:27.673692Z","shell.execute_reply.started":"2026-08-08T19:45:27.648464Z","shell.execute_reply":"2026-08-08T19:45:27.672574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 20 — TEST PREDICTION & KAGGLE SUBMISSION\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nimport pydicom\n\nprint(\"=\" * 70)\nprint(\"STEP 20 — TEST PREDICTION & SUBMISSION\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# PATHS\n# ------------------------------------------------------------\n\nBASE = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\ntest_csv_path = os.path.join(BASE, \"test.csv\")\ntest_series_csv_path = os.path.join(BASE, \"test_series.csv\")\ntest_series_root = os.path.join(BASE, \"test_series\")\n\nsample_submission_path = os.path.join(\n    BASE, \"sample_submission.csv\"\n)\n\nmodel_path = \"/kaggle/working/best_knee_resnet18.pth\"\n\nsubmission_path = \"/kaggle/working/submission.csv\"\n\n\n# ------------------------------------------------------------\n# LABELS\n# ------------------------------------------------------------\n\nlabel_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\nprint(\"Number of labels:\", len(label_cols))\n\n\n# ------------------------------------------------------------\n# LOAD TEST CSV\n# ------------------------------------------------------------\n\ntest_df = pd.read_csv(test_csv_path)\ntest_series_df = pd.read_csv(test_series_csv_path)\n\nprint(\"Test studies:\", len(test_df))\nprint(\"Test series:\", len(test_series_df))\n\n\n# ------------------------------------------------------------\n# TEST DATASET\n# ------------------------------------------------------------\n\nclass TestKneeMRIDataset(Dataset):\n\n    def __init__(\n        self,\n        study_df,\n        series_root,\n        num_images=8,\n        image_size=224\n    ):\n        self.study_df = study_df.reset_index(drop=True)\n        self.series_root = series_root\n        self.num_images = num_images\n        self.image_size = image_size\n\n    def __len__(self):\n        return len(self.study_df)\n\n    def _get_dicom_files(self, study_id):\n\n        study_path = os.path.join(\n            self.series_root,\n            str(study_id)\n        )\n\n        files = []\n\n        if os.path.exists(study_path):\n\n            for root, dirs, filenames in os.walk(study_path):\n\n                for filename in filenames:\n\n                    if filename.lower().endswith(\".dcm\"):\n                        files.append(\n                            os.path.join(root, filename)\n                        )\n\n        return sorted(files)\n\n    def _read_dicom(self, path):\n\n        ds = pydicom.dcmread(path)\n\n        image = ds.pixel_array.astype(np.float32)\n\n        # Normalize\n        image -= image.min()\n\n        if image.max() > 0:\n            image /= image.max()\n\n        image = (image * 255).astype(np.uint8)\n\n        image = Image.fromarray(image)\n\n        image = image.resize(\n            (self.image_size, self.image_size)\n        )\n\n        image = np.array(image).astype(np.float32) / 255.0\n\n        # grayscale -> RGB\n        image = np.stack(\n            [image, image, image],\n            axis=0\n        )\n\n        return torch.tensor(\n            image,\n            dtype=torch.float32\n        )\n\n    def __getitem__(self, index):\n\n        study_id = self.study_df.iloc[index][\"StudyInstanceUID\"]\n\n        files = self._get_dicom_files(study_id)\n\n        # If fewer than 8 images\n        if len(files) == 0:\n\n            images = torch.zeros(\n                self.num_images,\n                3,\n                self.image_size,\n                self.image_size\n            )\n\n        else:\n\n            # Select 8 evenly distributed images\n            if len(files) >= self.num_images:\n\n                indices = np.linspace(\n                    0,\n                    len(files) - 1,\n                    self.num_images\n                ).astype(int)\n\n                selected = [\n                    files[i]\n                    for i in indices\n                ]\n\n            else:\n\n                selected = files.copy()\n\n                while len(selected) < self.num_images:\n                    selected.append(files[-1])\n\n            image_list = []\n\n            for file in selected:\n\n                try:\n\n                    image = self._read_dicom(file)\n                    image_list.append(image)\n\n                except Exception:\n\n                    image_list.append(\n                        torch.zeros(\n                            3,\n                            self.image_size,\n                            self.image_size\n                        )\n                    )\n\n            images = torch.stack(image_list)\n\n        return images, study_id\n\n\n# ------------------------------------------------------------\n# CREATE TEST DATASET\n# ------------------------------------------------------------\n\ntest_dataset = TestKneeMRIDataset(\n    test_df,\n    test_series_root,\n    num_images=8,\n    image_size=224\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=1,\n    shuffle=False,\n    num_workers=0\n)\n\nprint()\nprint(\"Test dataset created!\")\nprint(\"Number of test studies:\", len(test_dataset))\n\n\n# ------------------------------------------------------------\n# LOAD MODEL\n# ------------------------------------------------------------\n\nprint()\nprint(\"Loading best model...\")\n\n# ResNet18\nfrom torchvision.models import resnet18\n\nmodel = resnet18(weights=None)\n\nmodel.fc = nn.Linear(\n    model.fc.in_features,\n    len(label_cols)\n)\n\ncheckpoint = torch.load(\n    model_path,\n    map_location=\"cpu\"\n)\n\nif isinstance(checkpoint, dict) and \"state_dict\" in checkpoint:\n    model.load_state_dict(checkpoint[\"state_dict\"])\nelse:\n    model.load_state_dict(checkpoint)\n\ndevice = torch.device(\"cpu\")\n\nmodel = model.to(device)\nmodel.eval()\n\nprint(\"Best model loaded successfully!\")\nprint(\"Device:\", device)\n\n\n# ------------------------------------------------------------\n# PREDICTION\n# ------------------------------------------------------------\n\nprint()\nprint(\"Generating test predictions...\")\n\nall_probabilities = []\nall_study_ids = []\n\nwith torch.no_grad():\n\n    for batch_index, (images, study_ids) in enumerate(test_loader):\n\n        # [1, 8, 3, 224, 224]\n        images = images.to(device)\n\n        batch_size, num_images, channels, height, width = images.shape\n\n        # Process 8 MRI images through ResNet18\n        images_flat = images.view(\n            batch_size * num_images,\n            channels,\n            height,\n            width\n        )\n\n        logits = model(images_flat)\n\n        # Average predictions from 8 images\n        logits = logits.view(\n            batch_size,\n            num_images,\n            len(label_cols)\n        )\n\n        logits = logits.mean(dim=1)\n\n        probabilities = torch.sigmoid(logits)\n\n        all_probabilities.append(\n            probabilities.cpu().numpy()\n        )\n\n        all_study_ids.extend(\n            list(study_ids)\n        )\n\n        print(\n            f\"Processed {batch_index + 1}/{len(test_loader)}\"\n        )\n\n\n# ------------------------------------------------------------\n# COMBINE PREDICTIONS\n# ------------------------------------------------------------\n\nprobabilities = np.concatenate(\n    all_probabilities,\n    axis=0\n)\n\nprint()\nprint(\"Prediction shape:\", probabilities.shape)\nprint(\"Study IDs:\", len(all_study_ids))\n\n\n# ------------------------------------------------------------\n# CREATE SUBMISSION\n# ------------------------------------------------------------\n\nsubmission = pd.DataFrame(\n    probabilities,\n    columns=label_cols\n)\n\nsubmission.insert(\n    0,\n    \"StudyInstanceUID\",\n    all_study_ids\n)\n\n\n# Make sure order matches sample submission\nsample_submission = pd.read_csv(\n    sample_submission_path\n)\n\nsubmission = sample_submission[\n    [\"StudyInstanceUID\"] + label_cols\n].copy()\n\n# Match predictions by StudyInstanceUID\nprediction_df = pd.DataFrame(\n    probabilities,\n    columns=label_cols\n)\n\nprediction_df.insert(\n    0,\n    \"StudyInstanceUID\",\n    all_study_ids\n)\n\nsubmission = sample_submission[\n    [\"StudyInstanceUID\"]\n].merge(\n    prediction_df,\n    on=\"StudyInstanceUID\",\n    how=\"left\"\n)\n\n\n# ------------------------------------------------------------\n# SAVE\n# ------------------------------------------------------------\n\nsubmission.to_csv(\n    submission_path,\n    index=False\n)\n\nprint()\nprint(\"=\" * 70)\nprint(\"SUBMISSION CREATED SUCCESSFULLY\")\nprint(\"=\" * 70)\n\nprint(\"File:\")\nprint(submission_path)\n\nprint(\"Shape:\", submission.shape)\n\nprint()\nprint(\"First rows:\")\n\ndisplay(submission.head())\n\nprint()\nprint(\"Checking missing values:\")\n\nprint(\n    submission.isna().sum().sum(),\n    \"missing values\"\n)\n\nprint()\nprint(\"Checking probability range:\")\n\nprint(\n    \"Minimum:\",\n    submission[label_cols].min().min()\n)\n\nprint(\n    \"Maximum:\",\n    submission[label_cols].max().max()\n)\n\nprint()\nprint(\"=\" * 70)\nprint(\"STEP 20 COMPLETED\")\nprint(\"=\" * 70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:47:37.232326Z","iopub.execute_input":"2026-08-08T19:47:37.232739Z","iopub.status.idle":"2026-08-08T19:47:44.162295Z","shell.execute_reply.started":"2026-08-08T19:47:37.2327Z","shell.execute_reply":"2026-08-08T19:47:44.160623Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Searching for best_knee_resnet18.pth ...\")\n\nfound = []\n\nfor root, dirs, files in os.walk(\"/kaggle\"):\n    for file in files:\n        if file == \"best_knee_resnet18.pth\":\n            found.append(os.path.join(root, file))\n\nprint(\"\\nFound files:\")\n\nif found:\n    for f in found:\n        print(f)\nelse:\n    print(\"❌ Model file not found\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T19:49:07.016823Z","iopub.execute_input":"2026-08-08T19:49:07.017706Z","iopub.status.idle":"2026-08-08T19:55:54.337001Z","shell.execute_reply.started":"2026-08-08T19:49:07.017665Z","shell.execute_reply":"2026-08-08T19:55:54.335529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Searching model...\")\n\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    for file in files:\n        if file == \"best_knee_resnet18.pth\":\n            print(\"MODEL FOUND:\")\n            print(os.path.join(root, file))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:31:12.686557Z","iopub.execute_input":"2026-08-08T20:31:12.68958Z","iopub.status.idle":"2026-08-08T20:33:42.724743Z","shell.execute_reply.started":"2026-08-08T20:31:12.689501Z","shell.execute_reply":"2026-08-08T20:33:42.722372Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Kaggle Input folders:\")\n\nfor item in os.listdir(\"/kaggle/input\"):\n    print(\" -\", item)\n\nprint(\"\\nAll files:\")\n\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    for file in files:\n        print(os.path.join(root, file))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-08T20:34:26.892938Z","iopub.execute_input":"2026-08-08T20:34:26.893289Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(os.listdir(\"/kaggle/input\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T08:58:34.835677Z","iopub.execute_input":"2026-08-09T08:58:34.83661Z","iopub.status.idle":"2026-08-09T08:58:34.848737Z","shell.execute_reply.started":"2026-08-09T08:58:34.836572Z","shell.execute_reply":"2026-08-09T08:58:34.847765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"DATASETS FOLDER:\")\nprint(os.listdir(\"/kaggle/input/datasets\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T09:00:26.66469Z","iopub.execute_input":"2026-08-09T09:00:26.665128Z","iopub.status.idle":"2026-08-09T09:00:26.673314Z","shell.execute_reply.started":"2026-08-09T09:00:26.665094Z","shell.execute_reply":"2026-08-09T09:00:26.672056Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\npath = \"/kaggle/input/datasets/jamshaidmemon\"\n\nprint(\"Folder contents:\")\nprint(os.listdir(path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T09:01:31.693827Z","iopub.execute_input":"2026-08-09T09:01:31.694529Z","iopub.status.idle":"2026-08-09T09:01:31.700457Z","shell.execute_reply.started":"2026-08-09T09:01:31.694494Z","shell.execute_reply":"2026-08-09T09:01:31.699347Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nmodel_folder = \"/kaggle/input/datasets/jamshaidmemon/knee-mri-final-model\"\n\nprint(\"MODEL FOLDER:\")\nprint(os.listdir(model_folder))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T09:02:18.215975Z","iopub.execute_input":"2026-08-09T09:02:18.216922Z","iopub.status.idle":"2026-08-09T09:02:18.231122Z","shell.execute_reply.started":"2026-08-09T09:02:18.216863Z","shell.execute_reply":"2026-08-09T09:02:18.229281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport os\n\nmodel_path = \"/kaggle/input/datasets/jamshaidmemon/knee-mri-final-model/best_knee_resnet18.pth\"\n\nprint(\"Checking model...\")\n\nprint(\"Exists:\", os.path.exists(model_path))\nprint(\"Size:\", round(os.path.getsize(model_path) / (1024 * 1024), 2), \"MB\")\n\ncheckpoint = torch.load(\n    model_path,\n    map_location=\"cpu\"\n)\n\nprint(\"MODEL LOADED SUCCESSFULLY!\")\nprint(\"Checkpoint type:\", type(checkpoint))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T09:03:08.097742Z","iopub.execute_input":"2026-08-09T09:03:08.098239Z","iopub.status.idle":"2026-08-09T09:03:12.633725Z","shell.execute_reply.started":"2026-08-09T09:03:08.098206Z","shell.execute_reply":"2026-08-09T09:03:12.632554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 26 — TEST PREDICTION + SUBMISSION.CSV\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport pydicom\n\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import resnet18\n\nprint(\"=\" * 70)\nprint(\"STEP 26 — TEST PREDICTION\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# PATHS\n# ------------------------------------------------------------\n\nBASE = \"/kaggle/input/competitions/rsna-knee-abnormality-detection\"\n\ntest_csv_path = os.path.join(BASE, \"test.csv\")\ntest_series_csv_path = os.path.join(BASE, \"test_series.csv\")\ntest_series_root = os.path.join(BASE, \"test_series\")\nsample_submission_path = os.path.join(BASE, \"sample_submission.csv\")\n\nmodel_path = (\n    \"/kaggle/input/datasets/jamshaidmemon/\"\n    \"knee-mri-final-model/best_knee_resnet18.pth\"\n)\n\nsubmission_path = \"/kaggle/working/submission.csv\"\n\n\n# ------------------------------------------------------------\n# LABELS\n# ------------------------------------------------------------\n\nlabel_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\nprint(\"Number of labels:\", len(label_cols))\n\n\n# ------------------------------------------------------------\n# LOAD TEST DATA\n# ------------------------------------------------------------\n\ntest_df = pd.read_csv(test_csv_path)\ntest_series_df = pd.read_csv(test_series_csv_path)\nsample_submission = pd.read_csv(sample_submission_path)\n\nprint(\"Test studies:\", len(test_df))\nprint(\"Test series:\", len(test_series_df))\nprint(\"Sample submission shape:\", sample_submission.shape)\n\n\n# ------------------------------------------------------------\n# DATASET\n# ------------------------------------------------------------\n\nclass TestKneeMRIDataset(Dataset):\n\n    def __init__(\n        self,\n        study_df,\n        series_root,\n        num_images=8,\n        image_size=224\n    ):\n        self.study_df = study_df.reset_index(drop=True)\n        self.series_root = series_root\n        self.num_images = num_images\n        self.image_size = image_size\n\n    def __len__(self):\n        return len(self.study_df)\n\n    def get_dicom_files(self, study_id):\n\n        study_path = os.path.join(\n            self.series_root,\n            str(study_id)\n        )\n\n        files = []\n\n        if os.path.exists(study_path):\n\n            for root, dirs, filenames in os.walk(study_path):\n\n                for filename in filenames:\n\n                    if filename.lower().endswith(\".dcm\"):\n                        files.append(\n                            os.path.join(root, filename)\n                        )\n\n        return sorted(files)\n\n    def read_dicom(self, path):\n\n        ds = pydicom.dcmread(path)\n\n        image = ds.pixel_array.astype(np.float32)\n\n        image -= image.min()\n\n        if image.max() > 0:\n            image /= image.max()\n\n        image = (image * 255).astype(np.uint8)\n\n        image = Image.fromarray(image)\n\n        image = image.resize(\n            (self.image_size, self.image_size)\n        )\n\n        image = np.asarray(image).astype(\n            np.float32\n        ) / 255.0\n\n        # Grayscale → RGB\n        image = np.stack(\n            [image, image, image],\n            axis=0\n        )\n\n        return torch.tensor(\n            image,\n            dtype=torch.float32\n        )\n\n    def __getitem__(self, index):\n\n        study_id = self.study_df.iloc[\n            index\n        ][\"StudyInstanceUID\"]\n\n        files = self.get_dicom_files(\n            study_id\n        )\n\n        if len(files) == 0:\n\n            images = torch.zeros(\n                self.num_images,\n                3,\n                self.image_size,\n                self.image_size\n            )\n\n        else:\n\n            if len(files) >= self.num_images:\n\n                indices = np.linspace(\n                    0,\n                    len(files) - 1,\n                    self.num_images\n                ).astype(int)\n\n                selected = [\n                    files[i]\n                    for i in indices\n                ]\n\n            else:\n\n                selected = files.copy()\n\n                while len(selected) < self.num_images:\n                    selected.append(files[-1])\n\n            image_list = []\n\n            for file in selected:\n\n                try:\n                    image = self.read_dicom(file)\n\n                except Exception:\n\n                    image = torch.zeros(\n                        3,\n                        self.image_size,\n                        self.image_size\n                    )\n\n                image_list.append(image)\n\n            images = torch.stack(image_list)\n\n        return images, str(study_id)\n\n\n# ------------------------------------------------------------\n# CREATE DATASET\n# ------------------------------------------------------------\n\ntest_dataset = TestKneeMRIDataset(\n    test_df,\n    test_series_root,\n    num_images=8,\n    image_size=224\n)\n\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=1,\n    shuffle=False,\n    num_workers=0\n)\n\nprint()\nprint(\"TEST DATASET READY\")\nprint(\"Number of studies:\", len(test_dataset))\n\n\n# ------------------------------------------------------------\n# LOAD MODEL\n# ------------------------------------------------------------\n\ndevice = torch.device(\"cpu\")\n\nmodel = resnet18(weights=None)\n\nmodel.fc = nn.Linear(\n    model.fc.in_features,\n    len(label_cols)\n)\n\ncheckpoint = torch.load(\n    model_path,\n    map_location=\"cpu\"\n)\n\nmodel.load_state_dict(checkpoint)\n\nmodel = model.to(device)\nmodel.eval()\n\nprint()\nprint(\"MODEL READY\")\nprint(\"Device:\", device)\n\n\n# ------------------------------------------------------------\n# TEST PREDICTIONS\n# ------------------------------------------------------------\n\nprint()\nprint(\"GENERATING TEST PREDICTIONS...\")\n\nall_probabilities = []\nall_study_ids = []\n\nwith torch.no_grad():\n\n    for batch_index, (images, study_ids) in enumerate(\n        test_loader\n    ):\n\n        images = images.to(device)\n\n        batch_size, num_images, channels, height, width = images.shape\n\n        # [B, 8, 3, 224, 224]\n        # →\n        # [B*8, 3, 224, 224]\n\n        images_flat = images.reshape(\n            batch_size * num_images,\n            channels,\n            height,\n            width\n        )\n\n        logits = model(images_flat)\n\n        # Restore image dimension\n\n        logits = logits.reshape(\n            batch_size,\n            num_images,\n            len(label_cols)\n        )\n\n        # Average 8 MRI image predictions\n\n        logits = logits.mean(dim=1)\n\n        probabilities = torch.sigmoid(\n            logits\n        )\n\n        all_probabilities.append(\n            probabilities.cpu().numpy()\n        )\n\n        all_study_ids.extend(\n            study_ids\n        )\n\n        print(\n            f\"Processed {batch_index + 1}/\"\n            f\"{len(test_loader)}\"\n        )\n\n\n# ------------------------------------------------------------\n# COMBINE\n# ------------------------------------------------------------\n\nprobabilities = np.concatenate(\n    all_probabilities,\n    axis=0\n)\n\nprint()\nprint(\"Prediction shape:\", probabilities.shape)\nprint(\"Study IDs:\", len(all_study_ids))\n\n\n# ------------------------------------------------------------\n# CREATE PREDICTION DATAFRAME\n# ------------------------------------------------------------\n\nprediction_df = pd.DataFrame(\n    probabilities,\n    columns=label_cols\n)\n\nprediction_df.insert(\n    0,\n    \"StudyInstanceUID\",\n    all_study_ids\n)\n\n\n# ------------------------------------------------------------\n# MATCH SAMPLE SUBMISSION ORDER\n# ------------------------------------------------------------\n\nsubmission = sample_submission[\n    [\"StudyInstanceUID\"]\n].merge(\n    prediction_df,\n    on=\"StudyInstanceUID\",\n    how=\"left\"\n)\n\n\n# ------------------------------------------------------------\n# CHECK\n# ------------------------------------------------------------\n\nprint()\nprint(\"SUBMISSION CHECK\")\nprint(\"Shape:\", submission.shape)\n\nprint(\n    \"Missing values:\",\n    submission.isna().sum().sum()\n)\n\nprint(\n    \"Minimum probability:\",\n    submission[label_cols].min().min()\n)\n\nprint(\n    \"Maximum probability:\",\n    submission[label_cols].max().max()\n)\n\n\n# ------------------------------------------------------------\n# SAVE\n# ------------------------------------------------------------\n\nsubmission.to_csv(\n    submission_path,\n    index=False\n)\n\nprint()\nprint(\"=\" * 70)\nprint(\"SUBMISSION.CSV CREATED SUCCESSFULLY\")\nprint(\"=\" * 70)\n\nprint(\"File:\")\nprint(submission_path)\n\nprint()\ndisplay(submission.head())\n\nprint()\nprint(\"=\" * 70)\nprint(\"STEP 26 COMPLETED\")\nprint(\"=\" * 70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T09:05:48.858917Z","iopub.execute_input":"2026-08-09T09:05:48.859672Z","iopub.status.idle":"2026-08-09T09:05:57.404588Z","shell.execute_reply.started":"2026-08-09T09:05:48.859638Z","shell.execute_reply":"2026-08-09T09:05:57.403484Z"}},"outputs":[],"execution_count":null}]}