{"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# RSNA Knee Abnormality Detection\n# Notebook 01 — Competition Reconnaissance\n# ============================================================\n\nimport os\nimport sys\nimport platform\nimport pandas as pd\nimport numpy as np\n\nprint(\"=\" * 60)\nprint(\"ENVIRONMENT\")\nprint(\"=\" * 60)\n\nprint(\"Python:\", sys.version)\nprint(\"Platform:\", platform.platform())\nprint(\"Pandas:\", pd.__version__)\nprint(\"NumPy:\", np.__version__)\n\nprint(\"\\nCurrent directory:\")\nprint(os.getcwd())\n\nprint(\"\\nTop-level files/directories:\")\nfor item in os.listdir(\"/kaggle/input\"):\n    print(\" -\", item)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T15:32:33.620845Z","iopub.execute_input":"2026-09-09T15:32:33.621184Z","iopub.status.idle":"2026-09-09T15:32:33.865748Z","shell.execute_reply.started":"2026-09-09T15:32:33.621154Z","shell.execute_reply":"2026-09-09T15:32:33.865058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Find RSNA competition files\n# ============================================================\n\nfor root, dirs, files in os.walk(\"/kaggle/input\"):\n    level = root.replace(\"/kaggle/input\", \"\").count(os.sep)\n    \n    if level <= 2:\n        print(root)\n        \n        for file in files[:20]:\n            print(\"   └──\", file)\n            ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T15:59:19.701344Z","iopub.execute_input":"2026-09-09T15:59:19.7016Z","iopub.status.idle":"2026-09-09T16:01:24.2377Z","shell.execute_reply.started":"2026-09-09T15:59:19.701578Z","shell.execute_reply":"2026-09-09T16:01:24.236201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(\"Datasets available in Kaggle:\")\nprint(os.listdir(\"/kaggle/input\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T15:58:58.842077Z","iopub.execute_input":"2026-09-09T15:58:58.842326Z","iopub.status.idle":"2026-09-09T15:58:58.846673Z","shell.execute_reply.started":"2026-09-09T15:58:58.842304Z","shell.execute_reply":"2026-09-09T15:58:58.845804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(os.listdir(\"/kaggle/input/datasets\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:01:30.34844Z","iopub.execute_input":"2026-09-09T16:01:30.348741Z","iopub.status.idle":"2026-09-09T16:01:30.354647Z","shell.execute_reply.started":"2026-09-09T16:01:30.348717Z","shell.execute_reply":"2026-09-09T16:01:30.353851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nprint(os.listdir(\"/kaggle/input/datasets/alenic\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:01:58.688424Z","iopub.execute_input":"2026-09-09T16:01:58.688756Z","iopub.status.idle":"2026-09-09T16:01:58.69355Z","shell.execute_reply.started":"2026-09-09T16:01:58.688733Z","shell.execute_reply":"2026-09-09T16:01:58.692652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nDATA_PATH = \"/kaggle/input/datasets/alenic/rsna-knee-abnormality-detection-jpeg-224x224\"\n\nprint(os.listdir(DATA_PATH))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:02:26.110383Z","iopub.execute_input":"2026-09-09T16:02:26.110684Z","iopub.status.idle":"2026-09-09T16:02:26.116014Z","shell.execute_reply.started":"2026-09-09T16:02:26.110657Z","shell.execute_reply":"2026-09-09T16:02:26.115195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport os\n\nDATA_PATH = \"/kaggle/input/datasets/alenic/rsna-knee-abnormality-detection-jpeg-224x224\"\n\ntrain = pd.read_csv(os.path.join(DATA_PATH, \"train.csv\"))\ntrain_series = pd.read_csv(os.path.join(DATA_PATH, \"train_series.csv\"))\n\ntest = pd.read_csv(os.path.join(DATA_PATH, \"test.csv\"))\ntest_series = pd.read_csv(os.path.join(DATA_PATH, \"test_series.csv\"))\n\nsample_submission = pd.read_csv(\n    os.path.join(DATA_PATH, \"sample_submission.csv\")\n)\n\nprint(\"Train:\", train.shape)\nprint(\"Train Series:\", train_series.shape)\nprint(\"Test:\", test.shape)\nprint(\"Test Series:\", test_series.shape)\nprint(\"Sample Submission:\", sample_submission.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:02:50.446734Z","iopub.execute_input":"2026-09-09T16:02:50.447032Z","iopub.status.idle":"2026-09-09T16:02:50.692897Z","shell.execute_reply.started":"2026-09-09T16:02:50.447006Z","shell.execute_reply":"2026-09-09T16:02:50.692244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"TRAIN COLUMNS:\")\nprint(train.columns.tolist())\n\nprint(\"\\nTRAIN SERIES COLUMNS:\")\nprint(train_series.columns.tolist())\n\nprint(\"\\nTEST COLUMNS:\")\nprint(test.columns.tolist())\n\nprint(\"\\nTEST SERIES COLUMNS:\")\nprint(test_series.columns.tolist())\n\nprint(\"\\nSUBMISSION COLUMNS:\")\nprint(sample_submission.columns.tolist())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:03:23.988508Z","iopub.execute_input":"2026-09-09T16:03:23.988783Z","iopub.status.idle":"2026-09-09T16:03:23.993963Z","shell.execute_reply.started":"2026-09-09T16:03:23.988758Z","shell.execute_reply":"2026-09-09T16:03:23.992893Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_columns = [\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 studies:\", len(train))\nprint(\"\\nTarget distribution:\\n\")\n\nfor col in target_columns:\n    positive = train[col].sum()\n    negative = train[col].notna().sum() - positive\n    percentage = positive / train[col].notna().sum() * 100\n    \n    print(\n        f\"{col:20s} | \"\n        f\"Positive: {positive:.0f} | \"\n        f\"Negative: {negative:.0f} | \"\n        f\"Positive %: {percentage:.2f}%\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:04:47.120433Z","iopub.execute_input":"2026-09-09T16:04:47.120744Z","iopub.status.idle":"2026-09-09T16:04:47.132677Z","shell.execute_reply.started":"2026-09-09T16:04:47.120716Z","shell.execute_reply":"2026-09-09T16:04:47.131466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Missing labels per target:\")\nprint(train[target_columns].isna().sum())\n\nprint(\"\\nNumber of completely labeled studies:\")\nprint(train[target_columns].notna().all(axis=1).sum())\n\nprint(\"\\nNumber of studies with at least one label:\")\nprint(train[target_columns].notna().any(axis=1).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:05:12.860558Z","iopub.execute_input":"2026-09-09T16:05:12.860831Z","iopub.status.idle":"2026-09-09T16:05:12.873298Z","shell.execute_reply.started":"2026-09-09T16:05:12.8608Z","shell.execute_reply":"2026-09-09T16:05:12.872257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_mask = train[target_columns].notna().all(axis=1)\n\nprint(\"Officially labeled studies:\", labeled_mask.sum())\nprint(\"Reports-only / incomplete studies:\", (~labeled_mask).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:05:23.811485Z","iopub.execute_input":"2026-09-09T16:05:23.811807Z","iopub.status.idle":"2026-09-09T16:05:23.820565Z","shell.execute_reply.started":"2026-09-09T16:05:23.811769Z","shell.execute_reply":"2026-09-09T16:05:23.819381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_data = train[labeled_mask]\n\ndisplay(\n    labeled_data[\n        [\"Report\"] + target_columns\n    ].head(10)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:05:37.550272Z","iopub.execute_input":"2026-09-09T16:05:37.550544Z","iopub.status.idle":"2026-09-09T16:05:37.584155Z","shell.execute_reply.started":"2026-09-09T16:05:37.550517Z","shell.execute_reply":"2026-09-09T16:05:37.583194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.set_option(\"display.max_colwidth\", 500)\n\nfor i, row in labeled_data.iterrows():\n    print(\"=\" * 100)\n    print(\"Study:\", row[\"StudyInstanceUID\"])\n    print(row[\"Report\"])\n    print(\"\\nLabels:\")\n    \n    for col in target_columns:\n        print(f\"{col}: {int(row[col])}\", end=\" | \")\n    \n    print(\"\\n\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:06:20.699539Z","iopub.execute_input":"2026-09-09T16:06:20.69984Z","iopub.status.idle":"2026-09-09T16:06:20.729106Z","shell.execute_reply.started":"2026-09-09T16:06:20.699816Z","shell.execute_reply":"2026-09-09T16:06:20.728136Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check whether any label-positive study has\n# an obviously \"normal\" style report.\n\nfor col in target_columns:\n    positive_rows = labeled_data[labeled_data[col] == 1]\n\n    print(f\"\\n{'='*60}\")\n    print(f\"{col} — {len(positive_rows)} positive studies\")\n\n    for _, row in positive_rows.head(5).iterrows():\n        print(\"\\nReport:\")\n        print(row[\"Report\"][:1000])\n        print(\"Label:\", int(row[col]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:08:00.496046Z","iopub.execute_input":"2026-09-09T16:08:00.496316Z","iopub.status.idle":"2026-09-09T16:08:00.511614Z","shell.execute_reply.started":"2026-09-09T16:08:00.496292Z","shell.execute_reply":"2026-09-09T16:08:00.510795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_columns = [\n    'ACL','MCL','Medial Meniscus','Lateral Meniscus',\n    'Medial OA','Lateral OA','PF OA','Effusion',\n    'Synovitis',\"Baker's\",'Contusion','Fracture'\n]\n\nprint(\"Total training studies:\", len(train))\n\nprint(\"\\nLabels available for each target:\")\ndisplay(train[target_columns].notna().sum())\n\nprint(\"\\nStudies with ALL 12 official labels:\")\nprint(train[target_columns].notna().all(axis=1).sum())\n\nprint(\"\\nStudies without complete labels:\")\nprint((~train[target_columns].notna().all(axis=1)).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:09:59.166135Z","iopub.execute_input":"2026-09-09T16:09:59.166529Z","iopub.status.idle":"2026-09-09T16:09:59.185712Z","shell.execute_reply.started":"2026-09-09T16:09:59.166497Z","shell.execute_reply":"2026-09-09T16:09:59.18412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\n\ndef detect_acl(report):\n    report = str(report).lower()\n\n    # Negative statements first\n    negative_patterns = [\n        r'no acl tear',\n        r'no acl rupture',\n        r'acl.*intact',\n        r'acl.*normal',\n        r'intact.*acl',\n        r'normal.*acl'\n    ]\n\n    # Positive statements\n    positive_patterns = [\n        r'acl tear',\n        r'acl rupture',\n        r'acl is torn',\n        r'acl.*rupture',\n        r'anterior cruciate ligament.*tear',\n        r'anterior cruciate ligament.*rupture',\n        r'lca.*ruptur'\n    ]\n\n    for pattern in negative_patterns:\n        if re.search(pattern, report):\n            return 0\n\n    for pattern in positive_patterns:\n        if re.search(pattern, report):\n            return 1\n\n    return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:13:53.827013Z","iopub.execute_input":"2026-09-09T16:13:53.827377Z","iopub.status.idle":"2026-09-09T16:13:53.834256Z","shell.execute_reply.started":"2026-09-09T16:13:53.827344Z","shell.execute_reply":"2026-09-09T16:13:53.833245Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_mask = train[target_columns].notna().all(axis=1)\n\nlabeled_data = train[labeled_mask].copy()\n\nprint(\"Labeled studies:\", len(labeled_data))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:14:47.543022Z","iopub.execute_input":"2026-09-09T16:14:47.543365Z","iopub.status.idle":"2026-09-09T16:14:47.553532Z","shell.execute_reply.started":"2026-09-09T16:14:47.543335Z","shell.execute_reply":"2026-09-09T16:14:47.552449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_data['ACL_pred'] = labeled_data['Report'].apply(detect_acl)\n\ndisplay(\n    labeled_data[\n        ['Report', 'ACL', 'ACL_pred']\n    ].head(20)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:14:58.476323Z","iopub.execute_input":"2026-09-09T16:14:58.476693Z","iopub.status.idle":"2026-09-09T16:14:58.493226Z","shell.execute_reply.started":"2026-09-09T16:14:58.476662Z","shell.execute_reply":"2026-09-09T16:14:58.49238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import precision_score, recall_score, f1_score\n\nacl_eval = labeled_data.dropna(subset=['ACL_pred']).copy()\n\ny_true = acl_eval['ACL'].astype(int)\ny_pred = acl_eval['ACL_pred'].astype(int)\n\nprint(\"Samples evaluated:\", len(acl_eval))\nprint(\"Precision:\", precision_score(y_true, y_pred, zero_division=0))\nprint(\"Recall:\", recall_score(y_true, y_pred, zero_division=0))\nprint(\"F1 Score:\", f1_score(y_true, y_pred, zero_division=0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:15:12.912136Z","iopub.execute_input":"2026-09-09T16:15:12.912504Z","iopub.status.idle":"2026-09-09T16:15:13.805341Z","shell.execute_reply.started":"2026-09-09T16:15:12.912476Z","shell.execute_reply":"2026-09-09T16:15:13.803815Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"id=\"q3m7vk\"\ndisplay(\n    labeled_data[\n        ['Report', 'ACL', 'ACL_pred']\n    ].to_string()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:15:47.068495Z","iopub.execute_input":"2026-09-09T16:15:47.069066Z","iopub.status.idle":"2026-09-09T16:15:47.0931Z","shell.execute_reply.started":"2026-09-09T16:15:47.069031Z","shell.execute_reply":"2026-09-09T16:15:47.090935Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i, row in labeled_data.iterrows():\n    print(\"=\" * 100)\n    print(\"ACTUAL ACL:\", row['ACL'])\n    print(\"PREDICTED:\", row['ACL_pred'])\n    print(\"REPORT:\")\n    print(row['Report'])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:16:05.598128Z","iopub.execute_input":"2026-09-09T16:16:05.598505Z","iopub.status.idle":"2026-09-09T16:16:05.614076Z","shell.execute_reply.started":"2026-09-09T16:16:05.598471Z","shell.execute_reply":"2026-09-09T16:16:05.613082Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\n\ndef detect_acl_v2(report):\n    text = str(report).lower()\n\n    # Normalize common variations\n    text = text.replace(\"anterior cruciate ligament\", \"acl\")\n    text = text.replace(\"cruciate ligament anterior\", \"acl\")\n\n    # Strong negative phrases\n    negative_patterns = [\n        r'no evidence of.*acl',\n        r'no.*acl.*tear',\n        r'no.*acl.*rupture',\n        r'without.*acl.*tear',\n        r'acl.*intact',\n        r'acl.*normal',\n        r'acl.*preserved',\n        r'acl.*unremarkable',\n        r'intact.*acl',\n        r'normal.*acl',\n    ]\n\n    for pattern in negative_patterns:\n        if re.search(pattern, text):\n            return 0\n\n    # Strong positive phrases\n    positive_patterns = [\n        r'acl.*complete.*tear',\n        r'acl.*partial.*tear',\n        r'acl.*tear',\n        r'acl.*rupture',\n        r'acl.*ruptured',\n        r'acl.*injury',\n        r'acl.*lesion',\n        r'tear.*acl',\n        r'rupture.*acl',\n        r'ruptured.*acl',\n        r'injury.*acl',\n        r'lca.*ruptur',\n        r'vkb.*ruptur',\n    ]\n\n    for pattern in positive_patterns:\n        if re.search(pattern, text):\n            return 1\n\n    return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:17:03.688657Z","iopub.execute_input":"2026-09-09T16:17:03.689046Z","iopub.status.idle":"2026-09-09T16:17:03.696264Z","shell.execute_reply.started":"2026-09-09T16:17:03.689013Z","shell.execute_reply":"2026-09-09T16:17:03.695292Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_data['ACL_pred_v2'] = labeled_data['Report'].apply(detect_acl_v2)\n\nprint(\"Prediction counts:\")\nprint(labeled_data['ACL_pred_v2'].value_counts(dropna=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:17:14.459085Z","iopub.execute_input":"2026-09-09T16:17:14.459458Z","iopub.status.idle":"2026-09-09T16:17:14.476442Z","shell.execute_reply.started":"2026-09-09T16:17:14.459426Z","shell.execute_reply":"2026-09-09T16:17:14.475073Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import precision_score, recall_score, f1_score\n\nacl_eval = labeled_data.dropna(subset=['ACL_pred_v2']).copy()\n\ny_true = acl_eval['ACL'].astype(int)\ny_pred = acl_eval['ACL_pred_v2'].astype(int)\n\nprint(\"Samples evaluated:\", len(acl_eval))\nprint(\"Precision:\", precision_score(y_true, y_pred, zero_division=0))\nprint(\"Recall:\", recall_score(y_true, y_pred, zero_division=0))\nprint(\"F1 Score:\", f1_score(y_true, y_pred, zero_division=0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:17:32.306438Z","iopub.execute_input":"2026-09-09T16:17:32.306857Z","iopub.status.idle":"2026-09-09T16:17:32.32871Z","shell.execute_reply.started":"2026-09-09T16:17:32.306822Z","shell.execute_reply":"2026-09-09T16:17:32.327739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"errors = labeled_data[\n    labeled_data['ACL'].notna() &\n    labeled_data['ACL_pred_v2'].notna() &\n    (labeled_data['ACL'] != labeled_data['ACL_pred_v2'])\n]\n\nprint(\"Incorrect predictions:\", len(errors))\n\ndisplay(\n    errors[['Report', 'ACL', 'ACL_pred_v2']]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:17:43.314173Z","iopub.execute_input":"2026-09-09T16:17:43.314586Z","iopub.status.idle":"2026-09-09T16:17:43.330359Z","shell.execute_reply.started":"2026-09-09T16:17:43.314552Z","shell.execute_reply":"2026-09-09T16:17:43.329499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Prediction counts:\")\nprint(labeled_data['ACL_pred_v2'].value_counts(dropna=False))\n\nprint(\"\\nTotal labeled studies:\", len(labeled_data))\nprint(\"Recognized:\", labeled_data['ACL_pred_v2'].notna().sum())\nprint(\"Unknown:\", labeled_data['ACL_pred_v2'].isna().sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:18:19.636055Z","iopub.execute_input":"2026-09-09T16:18:19.636435Z","iopub.status.idle":"2026-09-09T16:18:19.645843Z","shell.execute_reply.started":"2026-09-09T16:18:19.636402Z","shell.execute_reply":"2026-09-09T16:18:19.643817Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\n\neval_data = labeled_data.dropna(subset=['ACL_pred_v2']).copy()\n\ncm = confusion_matrix(\n    eval_data['ACL'].astype(int),\n    eval_data['ACL_pred_v2'].astype(int)\n)\n\nprint(\"Confusion Matrix:\")\nprint(cm)\n\nprint(\"\\nRows = Actual\")\nprint(\"Columns = Predicted\")\nprint(\"        Pred 0   Pred 1\")\nprint(\"Actual 0       \", cm[0])\nprint(\"Actual 1       \", cm[1])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:18:35.269271Z","iopub.execute_input":"2026-09-09T16:18:35.269619Z","iopub.status.idle":"2026-09-09T16:18:35.287431Z","shell.execute_reply.started":"2026-09-09T16:18:35.269587Z","shell.execute_reply":"2026-09-09T16:18:35.286151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_negatives = labeled_data[\n    (labeled_data['ACL'] == 1) &\n    (labeled_data['ACL_pred_v2'] == 0)\n]\n\nprint(\"False Negatives:\", len(false_negatives))\n\nfor i, row in false_negatives.iterrows():\n    print(\"=\" * 100)\n    print(\"ACTUAL ACL:\", row['ACL'])\n    print(\"PREDICTED:\", row['ACL_pred_v2'])\n    print(\"REPORT:\")\n    print(row['Report'])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:19:41.554083Z","iopub.execute_input":"2026-09-09T16:19:41.554428Z","iopub.status.idle":"2026-09-09T16:19:41.562736Z","shell.execute_reply.started":"2026-09-09T16:19:41.554404Z","shell.execute_reply":"2026-09-09T16:19:41.561972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_positives = labeled_data[\n    (labeled_data['ACL'] == 0) &\n    (labeled_data['ACL_pred_v2'] == 1)\n]\n\nprint(\"False Positives:\", len(false_positives))\n\nfor i, row in false_positives.iterrows():\n    print(\"=\" * 100)\n    print(\"ACTUAL ACL:\", row['ACL'])\n    print(\"PREDICTED:\", row['ACL_pred_v2'])\n    print(\"REPORT:\")\n    print(row['Report'])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:19:59.003854Z","iopub.execute_input":"2026-09-09T16:19:59.005069Z","iopub.status.idle":"2026-09-09T16:19:59.01592Z","shell.execute_reply.started":"2026-09-09T16:19:59.004973Z","shell.execute_reply":"2026-09-09T16:19:59.013531Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_negatives = labeled_data[\n    (labeled_data['ACL'] == 1) &\n    (labeled_data['ACL_pred_v2'] == 0)\n]\n\nprint(\"False Negatives:\", len(false_negatives))\n\nfor i, row in false_negatives.iterrows():\n    print(\"=\" * 100)\n    print(\"ACTUAL ACL:\", row['ACL'])\n    print(\"PREDICTED:\", row['ACL_pred_v2'])\n    print(\"REPORT:\")\n    print(row['Report'])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:20:53.960568Z","iopub.execute_input":"2026-09-09T16:20:53.960903Z","iopub.status.idle":"2026-09-09T16:20:53.970306Z","shell.execute_reply.started":"2026-09-09T16:20:53.960872Z","shell.execute_reply":"2026-09-09T16:20:53.969389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\n\ndef detect_acl_v3(report):\n    text = str(report).lower()\n\n    # Normalize terminology\n    text = text.replace(\"anterior cruciate ligament\", \"acl\")\n\n    # Turkish terminology seen in the dataset\n    # \"parsiyel rüptürü\" = partial rupture\n    # \"devamsızlık\" = discontinuity\n    # These are strong ACL-positive findings when near ACL.\n    \n    positive_patterns = [\n        # ACL + tear/rupture\n        r'\\bacl\\b.{0,80}\\b(tear|rupture|ruptured|injury|lesion)\\b',\n        r'\\b(tear|rupture|ruptured|injury|lesion)\\b.{0,80}\\bacl\\b',\n\n        # Explicit complete/high-grade/partial ACL injury\n        r'\\b(complete|partial|high[- ]grade|low[- ]grade|chronic|acute)\\b.{0,40}\\bacl\\b.{0,50}\\b(tear|rupture|injury)\\b',\n\n        # ACL mentioned after tear/rupture\n        r'\\bcomplete rupture acl\\b',\n        r'\\bcomplete tear acl\\b',\n        r'\\bacl rupture\\b',\n        r'\\bacl tear\\b',\n\n        # Turkish\n        r'\\bacl\\b.{0,100}\\bparsiyel rüptür\\b',\n        r'\\bacl\\b.{0,100}\\brüptür\\b',\n        r'\\bacl\\b.{0,100}\\bdevamsızlık\\b',\n\n        # LCA / VKB\n        r'\\blca\\b.{0,100}\\b(tear|rupture|ruptured|rüptur|ruptur)\\b',\n        r'\\bvkb\\b.{0,100}\\b(tear|rupture|ruptured|rüptur|ruptur)\\b',\n    ]\n\n    # Strong negative statements\n    negative_patterns = [\n        r'\\bno tear\\b.{0,40}\\bacl\\b',\n        r'\\bno rupture\\b.{0,40}\\bacl\\b',\n        r'\\bno evidence of\\b.{0,40}\\bacl\\b',\n        r'\\bacl\\b.{0,40}\\b(no tear|no rupture)\\b',\n        r'\\bacl\\b.{0,40}\\b(intact|normal|preserved|unremarkable)\\b',\n        r'\\bintact\\b.{0,40}\\bacl\\b',\n        r'\\bnormal\\b.{0,40}\\bacl\\b',\n    ]\n\n    # IMPORTANT:\n    # Only return 0 when the report explicitly says ACL is normal/intact.\n    for pattern in negative_patterns:\n        if re.search(pattern, text):\n            # Don't immediately return 0 if a strong positive statement\n            # also exists elsewhere in the report.\n            pass\n\n    # Strong positive evidence\n    for pattern in positive_patterns:\n        if re.search(pattern, text):\n            return 1\n\n    # Explicit negative evidence\n    for pattern in negative_patterns:\n        if re.search(pattern, text):\n            return 0\n\n    return None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:22:01.065393Z","iopub.execute_input":"2026-09-09T16:22:01.065747Z","iopub.status.idle":"2026-09-09T16:22:01.073633Z","shell.execute_reply.started":"2026-09-09T16:22:01.065717Z","shell.execute_reply":"2026-09-09T16:22:01.072474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_data['ACL_pred_v3'] = labeled_data['Report'].apply(detect_acl_v3)\n\nprint(\"Prediction counts:\")\nprint(labeled_data['ACL_pred_v3'].value_counts(dropna=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:22:13.74852Z","iopub.execute_input":"2026-09-09T16:22:13.748887Z","iopub.status.idle":"2026-09-09T16:22:13.776732Z","shell.execute_reply.started":"2026-09-09T16:22:13.748856Z","shell.execute_reply":"2026-09-09T16:22:13.775777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import precision_score, recall_score, f1_score\n\nacl_eval_v3 = labeled_data.dropna(subset=['ACL_pred_v3']).copy()\n\ny_true = acl_eval_v3['ACL'].astype(int)\ny_pred = acl_eval_v3['ACL_pred_v3'].astype(int)\n\nprint(\"Samples evaluated:\", len(acl_eval_v3))\nprint(\"Precision:\", precision_score(y_true, y_pred, zero_division=0))\nprint(\"Recall:\", recall_score(y_true, y_pred, zero_division=0))\nprint(\"F1 Score:\", f1_score(y_true, y_pred, zero_division=0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:22:25.115105Z","iopub.execute_input":"2026-09-09T16:22:25.115498Z","iopub.status.idle":"2026-09-09T16:22:25.140188Z","shell.execute_reply.started":"2026-09-09T16:22:25.115464Z","shell.execute_reply":"2026-09-09T16:22:25.139074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"errors_v3 = labeled_data[\n    labeled_data['ACL'].notna() &\n    labeled_data['ACL_pred_v3'].notna() &\n    (labeled_data['ACL'] != labeled_data['ACL_pred_v3'])\n]\n\nprint(\"Incorrect predictions:\", len(errors_v3))\n\ndisplay(\n    errors_v3[['Report', 'ACL', 'ACL_pred_v3']]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:22:35.745276Z","iopub.execute_input":"2026-09-09T16:22:35.745619Z","iopub.status.idle":"2026-09-09T16:22:35.759947Z","shell.execute_reply.started":"2026-09-09T16:22:35.745595Z","shell.execute_reply":"2026-09-09T16:22:35.758472Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import precision_score, recall_score, f1_score\n\nacl_eval_v3 = labeled_data.dropna(subset=['ACL_pred_v3']).copy()\n\ny_true = acl_eval_v3['ACL'].astype(int)\ny_pred = acl_eval_v3['ACL_pred_v3'].astype(int)\n\nprint(\"Samples evaluated:\", len(acl_eval_v3))\nprint(\"Precision:\", precision_score(y_true, y_pred, zero_division=0))\nprint(\"Recall:\", recall_score(y_true, y_pred, zero_division=0))\nprint(\"F1 Score:\", f1_score(y_true, y_pred, zero_division=0))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:23:37.014347Z","iopub.execute_input":"2026-09-09T16:23:37.014694Z","iopub.status.idle":"2026-09-09T16:23:37.036195Z","shell.execute_reply.started":"2026-09-09T16:23:37.014664Z","shell.execute_reply":"2026-09-09T16:23:37.035487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\n\ncm = confusion_matrix(y_true, y_pred)\n\nprint(\"Confusion Matrix:\")\nprint(cm)\n\nprint(\"\\nRows = Actual\")\nprint(\"Columns = Predicted\")\nprint(\"Actual 0:\", cm[0])\nprint(\"Actual 1:\", cm[1])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:23:48.429163Z","iopub.execute_input":"2026-09-09T16:23:48.429514Z","iopub.status.idle":"2026-09-09T16:23:48.43875Z","shell.execute_reply.started":"2026-09-09T16:23:48.429484Z","shell.execute_reply":"2026-09-09T16:23:48.437873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\n\ndef detect_acl_v4(report):\n    text = str(report).lower()\n\n    # Normalize terminology\n    text = text.replace(\"anterior cruciate ligament\", \"acl\")\n    text = text.replace(\"ligamentum cruciatum anterius\", \"acl\")\n\n    # Split report into sentences\n    sentences = re.split(r'(?<=[.!?])\\s+', text)\n\n    for sentence in sentences:\n\n        # Only analyze sentences that mention ACL\n        if not re.search(r'\\bacl\\b|\\blca\\b|\\bvkb\\b', sentence):\n            continue\n\n        # -----------------------------------\n        # NEGATIVE / NORMAL ACL\n        # -----------------------------------\n\n        negative_patterns = [\n            r'\\bno\\s+(?:evidence\\s+of\\s+)?(?:a\\s+)?(?:tear|rupture)\\b',\n            r'\\bwithout\\s+(?:a\\s+)?(?:tear|rupture)\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\b(?:intact|normal|preserved)\\b',\n            r'\\b(?:intact|normal|preserved)\\b.*\\b(?:acl|lca|vkb)\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\bno\\s+(?:tear|rupture)\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\bunremarkable\\b',\n        ]\n\n        is_negative = any(\n            re.search(pattern, sentence)\n            for pattern in negative_patterns\n        )\n\n        if is_negative:\n            continue\n\n        # -----------------------------------\n        # POSITIVE ACL\n        # -----------------------------------\n\n        positive_patterns = [\n            r'\\b(?:acl|lca|vkb)\\b.*\\b(?:tear|rupture|ruptured)\\b',\n            r'\\b(?:tear|rupture|ruptured)\\b.*\\b(?:acl|lca|vkb)\\b',\n\n            r'\\b(?:acl|lca|vkb)\\b.*\\b(?:injury|lesion)\\b',\n            r'\\b(?:injury|lesion)\\b.*\\b(?:acl|lca|vkb)\\b',\n\n            r'\\b(?:acl|lca|vkb)\\b.*\\b(?:partial|complete|high[- ]grade|low[- ]grade)\\b',\n            r'\\b(?:partial|complete|high[- ]grade|low[- ]grade)\\b.*\\b(?:acl|lca|vkb)\\b',\n\n            # Turkish terminology\n            r'\\b(?:acl|lca|vkb)\\b.*\\brüptür\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\bruptur\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\bparsiyel\\b',\n            r'\\b(?:acl|lca|vkb)\\b.*\\bdevamsızlık\\b',\n        ]\n\n        if any(\n            re.search(pattern, sentence)\n            for pattern in positive_patterns\n        ):\n            return 1\n\n    return 0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:24:42.926929Z","iopub.execute_input":"2026-09-09T16:24:42.927349Z","iopub.status.idle":"2026-09-09T16:24:42.936237Z","shell.execute_reply.started":"2026-09-09T16:24:42.927316Z","shell.execute_reply":"2026-09-09T16:24:42.935272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_data['ACL_pred_v4'] = labeled_data['Report'].apply(detect_acl_v4)\n\nfrom sklearn.metrics import (\n    precision_score,\n    recall_score,\n    f1_score,\n    confusion_matrix\n)\n\ny_true = labeled_data['ACL'].astype(int)\ny_pred = labeled_data['ACL_pred_v4'].astype(int)\n\nprint(\"Samples evaluated:\", len(labeled_data))\n\nprint(\"Precision:\", precision_score(y_true, y_pred, zero_division=0))\nprint(\"Recall:\", recall_score(y_true, y_pred, zero_division=0))\nprint(\"F1 Score:\", f1_score(y_true, y_pred, zero_division=0))\n\ncm = confusion_matrix(y_true, y_pred)\n\nprint(\"\\nConfusion Matrix:\")\nprint(cm)\n\nprint(\"\\nRows = Actual\")\nprint(\"Columns = Predicted\")\nprint(\"Actual 0:\", cm[0])\nprint(\"Actual 1:\", cm[1])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:24:53.963053Z","iopub.execute_input":"2026-09-09T16:24:53.963429Z","iopub.status.idle":"2026-09-09T16:24:53.991812Z","shell.execute_reply.started":"2026-09-09T16:24:53.963399Z","shell.execute_reply":"2026-09-09T16:24:53.990903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_negatives_v4 = labeled_data[\n    (labeled_data['ACL'] == 1) &\n    (labeled_data['ACL_pred_v4'] == 0)\n]\n\nprint(\"False Negatives:\", len(false_negatives_v4))\n\nfor i, (_, row) in enumerate(false_negatives_v4.iterrows(), 1):\n    print(\"\\n\" + \"=\"*80)\n    print(f\"FALSE NEGATIVE #{i}\")\n    print(\"=\"*80)\n    print(row['Report'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:26:02.630961Z","iopub.execute_input":"2026-09-09T16:26:02.631593Z","iopub.status.idle":"2026-09-09T16:26:02.650589Z","shell.execute_reply.started":"2026-09-09T16:26:02.631541Z","shell.execute_reply":"2026-09-09T16:26:02.647985Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_positives_v4 = labeled_data[\n    (labeled_data['ACL'] == 0) &\n    (labeled_data['ACL_pred_v4'] == 1)\n]\n\nprint(\"False Positives:\", len(false_positives_v4))\n\nfor i, (_, row) in enumerate(false_positives_v4.iterrows(), 1):\n    print(\"\\n\" + \"=\"*80)\n    print(f\"FALSE POSITIVE #{i}\")\n    print(\"=\"*80)\n    print(row['Report'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:26:20.066525Z","iopub.execute_input":"2026-09-09T16:26:20.066925Z","iopub.status.idle":"2026-09-09T16:26:20.077909Z","shell.execute_reply.started":"2026-09-09T16:26:20.06689Z","shell.execute_reply":"2026-09-09T16:26:20.076061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"false_negatives_v4 = labeled_data[\n    (labeled_data['ACL'] == 1) &\n    (labeled_data['ACL_pred_v4'] == 0)\n]\n\nprint(\"False Negatives:\", len(false_negatives_v4))\n\nfor i, (_, row) in enumerate(false_negatives_v4.iterrows(), 1):\n    print(\"\\n\" + \"=\"*80)\n    print(f\"FALSE NEGATIVE #{i}\")\n    print(\"=\"*80)\n    print(row['Report'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:27:59.774938Z","iopub.execute_input":"2026-09-09T16:27:59.775355Z","iopub.status.idle":"2026-09-09T16:27:59.785776Z","shell.execute_reply.started":"2026-09-09T16:27:59.77532Z","shell.execute_reply":"2026-09-09T16:27:59.784281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nprint(\"Total training series:\", len(train_series))\n\nprint(\"\\nAnatomical Plane:\")\nprint(train_series[\"Anatomical_Plane\"].value_counts(dropna=False))\n\nprint(\"\\nFluid Sensitive:\")\nprint(train_series[\"Fluid_Sensitive\"].value_counts(dropna=False))\n\nprint(\"\\nFat Suppression:\")\nprint(train_series[\"Fat_Suppression\"].value_counts(dropna=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:29:59.369286Z","iopub.execute_input":"2026-09-09T16:29:59.369696Z","iopub.status.idle":"2026-09-09T16:29:59.382931Z","shell.execute_reply.started":"2026-09-09T16:29:59.36966Z","shell.execute_reply":"2026-09-09T16:29:59.381637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nSeries per study:\")\nseries_per_study = train_series.groupby(\"StudyInstanceUID\").size()\n\nprint(series_per_study.describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:30:15.073858Z","iopub.execute_input":"2026-09-09T16:30:15.074306Z","iopub.status.idle":"2026-09-09T16:30:15.092877Z","shell.execute_reply.started":"2026-09-09T16:30:15.074268Z","shell.execute_reply":"2026-09-09T16:30:15.091261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nNumber of unique studies:\")\nprint(train_series[\"StudyInstanceUID\"].nunique())\n\nprint(\"\\nNumber of unique series:\")\nprint(train_series[\"SeriesInstanceUID\"].nunique())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:30:27.508272Z","iopub.execute_input":"2026-09-09T16:30:27.50867Z","iopub.status.idle":"2026-09-09T16:30:27.526653Z","shell.execute_reply.started":"2026-09-09T16:30:27.508633Z","shell.execute_reply":"2026-09-09T16:30:27.525131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"study_id = train_series[\"StudyInstanceUID\"].iloc[0]\n\nstudy_series = train_series[\n    train_series[\"StudyInstanceUID\"] == study_id\n]\n\ndisplay(study_series)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:30:45.989459Z","iopub.execute_input":"2026-09-09T16:30:45.989812Z","iopub.status.idle":"2026-09-09T16:30:46.004596Z","shell.execute_reply.started":"2026-09-09T16:30:45.98978Z","shell.execute_reply":"2026-09-09T16:30:46.002871Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\nTRAIN_SERIES_PATH = os.path.join(DATA_PATH, \"train_series\")\n\nprint(\"Exists:\", os.path.exists(TRAIN_SERIES_PATH))\n\nprint(\"\\nFirst 10 study folders:\")\n\nstudies = os.listdir(TRAIN_SERIES_PATH)\n\nfor x in studies[:10]:\n    print(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:30:56.889382Z","iopub.execute_input":"2026-09-09T16:30:56.889768Z","iopub.status.idle":"2026-09-09T16:30:56.904819Z","shell.execute_reply.started":"2026-09-09T16:30:56.889736Z","shell.execute_reply":"2026-09-09T16:30:56.903865Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"first_study = studies[0]\n\nstudy_path = os.path.join(\n    TRAIN_SERIES_PATH,\n    first_study\n)\n\nprint(\"Study:\", first_study)\nprint(\"Contents:\", os.listdir(study_path)[:10])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:31:07.709778Z","iopub.execute_input":"2026-09-09T16:31:07.710198Z","iopub.status.idle":"2026-09-09T16:31:07.718869Z","shell.execute_reply.started":"2026-09-09T16:31:07.710167Z","shell.execute_reply":"2026-09-09T16:31:07.717689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for root, dirs, files in os.walk(study_path):\n    image_files = [\n        f for f in files\n        if f.lower().endswith((\".jpg\", \".jpeg\", \".png\"))\n    ]\n\n    if image_files:\n        print(\"Folder:\", root)\n        print(\"Images:\", image_files[:10])\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:31:16.770266Z","iopub.execute_input":"2026-09-09T16:31:16.770681Z","iopub.status.idle":"2026-09-09T16:31:17.099501Z","shell.execute_reply.started":"2026-09-09T16:31:16.770642Z","shell.execute_reply":"2026-09-09T16:31:17.097451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from PIL import Image\nimport matplotlib.pyplot as plt\n\nimage_path = os.path.join(root, image_files[0])\n\nimg = Image.open(image_path)\n\nprint(\"Image size:\", img.size)\nprint(\"Image mode:\", img.mode)\n\nplt.figure(figsize=(6, 6))\nplt.imshow(img, cmap=\"gray\")\nplt.axis(\"off\")\nplt.title(\"Example MRI Slice\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:31:28.259444Z","iopub.execute_input":"2026-09-09T16:31:28.259777Z","iopub.status.idle":"2026-09-09T16:31:28.488855Z","shell.execute_reply.started":"2026-09-09T16:31:28.259754Z","shell.execute_reply":"2026-09-09T16:31:28.487121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Show the structure of the first study\nfirst_study = os.listdir(TRAIN_SERIES_PATH)[0]\n\nstudy_path = os.path.join(TRAIN_SERIES_PATH, first_study)\n\nprint(\"Study ID:\", first_study)\n\nfor item in os.listdir(study_path):\n    item_path = os.path.join(study_path, item)\n\n    if os.path.isdir(item_path):\n        images = [\n            f for f in os.listdir(item_path)\n            if f.lower().endswith((\".jpg\", \".jpeg\", \".png\"))\n        ]\n\n        print(\"\\nSeries:\", item)\n        print(\"Number of images:\", len(images))\n        print(\"First images:\", images[:5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:32:25.234875Z","iopub.execute_input":"2026-09-09T16:32:25.235288Z","iopub.status.idle":"2026-09-09T16:32:25.259485Z","shell.execute_reply.started":"2026-09-09T16:32:25.235255Z","shell.execute_reply":"2026-09-09T16:32:25.257812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Check only the first 10 studies\nstudies = os.listdir(TRAIN_SERIES_PATH)[:10]\n\nfor study_id in studies:\n    study_path = os.path.join(TRAIN_SERIES_PATH, study_id)\n\n    if not os.path.isdir(study_path):\n        continue\n\n    print(\"\\nStudy:\", study_id)\n\n    for series_id in os.listdir(study_path):\n        series_path = os.path.join(study_path, series_id)\n\n        if not os.path.isdir(series_path):\n            continue\n\n        images = os.listdir(series_path)\n\n        print(\n            \"  Series:\", series_id,\n            \"| Images:\", len(images)\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:37:42.629114Z","iopub.execute_input":"2026-09-09T16:37:42.629447Z","iopub.status.idle":"2026-09-09T16:37:42.731754Z","shell.execute_reply.started":"2026-09-09T16:37:42.62942Z","shell.execute_reply":"2026-09-09T16:37:42.730115Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# See which MRI series types are available\n\nprint(train_series[\"Anatomical_Plane\"].value_counts(dropna=False))\nprint(\"\\nFluid Sensitive:\")\nprint(train_series[\"Fluid_Sensitive\"].value_counts(dropna=False))\n\nprint(\"\\nFat Suppression:\")\nprint(train_series[\"Fat_Suppression\"].value_counts(dropna=False))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:38:17.149155Z","iopub.execute_input":"2026-09-09T16:38:17.149593Z","iopub.status.idle":"2026-09-09T16:38:17.15862Z","shell.execute_reply.started":"2026-09-09T16:38:17.149553Z","shell.execute_reply":"2026-09-09T16:38:17.157834Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 1: Get studies that have all 12 official labels\n# ============================================================\n\ntarget_columns = [\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\nlabeled_mask = train[target_columns].notna().all(axis=1)\n\nlabeled_studies = train.loc[\n    labeled_mask,\n    'StudyInstanceUID'\n].tolist()\n\nprint(\"Number of labeled studies:\", len(labeled_studies))\n\nprint(\"\\nFirst 5 study IDs:\")\nfor x in labeled_studies[:5]:\n    print(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:38:47.533193Z","iopub.execute_input":"2026-09-09T16:38:47.533559Z","iopub.status.idle":"2026-09-09T16:38:47.544933Z","shell.execute_reply.started":"2026-09-09T16:38:47.533525Z","shell.execute_reply":"2026-09-09T16:38:47.543449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 2: Find MRI series belonging to labeled studies\n# ============================================================\n\nlabeled_series = train_series[\n    train_series['StudyInstanceUID'].isin(labeled_studies)\n].copy()\n\nprint(\"Total series in labeled studies:\", len(labeled_series))\n\nprint(\"\\nAnatomical planes:\")\nprint(labeled_series['Anatomical_Plane'].value_counts())\n\nprint(\"\\nFluid/Fat suppression:\")\nprint(\n    labeled_series[\n        ['Fluid_Sensitive', 'Fat_Suppression']\n    ].value_counts()\n)\n\nprint(\"\\nFirst few series:\")\ndisplay(labeled_series.head(10))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:39:01.683564Z","iopub.execute_input":"2026-09-09T16:39:01.683948Z","iopub.status.idle":"2026-09-09T16:39:01.713837Z","shell.execute_reply.started":"2026-09-09T16:39:01.683912Z","shell.execute_reply":"2026-09-09T16:39:01.712729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 3 — Prepare labeled MRI series\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\n\n# Select fluid-sensitive + fat-suppressed series\ncandidate_series = labeled_series[\n    (labeled_series[\"Fluid_Sensitive\"] == 1) &\n    (labeled_series[\"Fat_Suppression\"] == 1)\n].copy()\n\nprint(\"Candidate series:\", len(candidate_series))\n\n# Prefer sagittal series\nsagittal_series = candidate_series[\n    candidate_series[\"Anatomical_Plane\"] == \"Sagittal\"\n].copy()\n\nprint(\"Sagittal fluid-sensitive/fat-suppressed series:\",\n      len(sagittal_series))\n\nprint(\"\\nSeries per labeled study:\")\nprint(\n    sagittal_series\n    .groupby(\"StudyInstanceUID\")\n    .size()\n    .describe()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:39:41.099294Z","iopub.execute_input":"2026-09-09T16:39:41.099702Z","iopub.status.idle":"2026-09-09T16:39:41.113917Z","shell.execute_reply.started":"2026-09-09T16:39:41.099665Z","shell.execute_reply":"2026-09-09T16:39:41.113092Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 4 — Create image paths\n# ============================================================\n\nTRAIN_SERIES_PATH = os.path.join(DATA_PATH, \"train_series\")\n\ndef get_image_files(study_id, series_id):\n    series_path = os.path.join(\n        TRAIN_SERIES_PATH,\n        study_id,\n        series_id\n    )\n\n    if not os.path.exists(series_path):\n        return []\n\n    files = [\n        f for f in os.listdir(series_path)\n        if f.lower().endswith((\".jpg\", \".jpeg\", \".png\"))\n    ]\n\n    # Sort so slices stay in consistent order\n    files.sort()\n\n    return [\n        os.path.join(series_path, f)\n        for f in files\n    ]\n\n\n# Test on first available series\nrow = sagittal_series.iloc[0]\n\nfiles = get_image_files(\n    row[\"StudyInstanceUID\"],\n    row[\"SeriesInstanceUID\"]\n)\n\nprint(\"Study:\", row[\"StudyInstanceUID\"])\nprint(\"Series:\", row[\"SeriesInstanceUID\"])\nprint(\"Number of images:\", len(files))\nprint(\"First image:\", files[0] if files else \"None\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:39:53.41723Z","iopub.execute_input":"2026-09-09T16:39:53.417541Z","iopub.status.idle":"2026-09-09T16:39:53.430322Z","shell.execute_reply.started":"2026-09-09T16:39:53.417517Z","shell.execute_reply":"2026-09-09T16:39:53.429415Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 5 — Build a small MRI dataset\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nNUM_SLICES = 8\n\nsamples = []\n\nfor _, row in sagittal_series.iterrows():\n\n    study_id = row[\"StudyInstanceUID\"]\n    series_id = row[\"SeriesInstanceUID\"]\n\n    image_files = get_image_files(study_id, series_id)\n\n    if len(image_files) < NUM_SLICES:\n        continue\n\n    # Evenly spaced slices\n    indices = np.linspace(\n        0,\n        len(image_files) - 1,\n        NUM_SLICES\n    ).astype(int)\n\n    selected_files = [\n        image_files[i] for i in indices\n    ]\n\n    samples.append({\n        \"StudyInstanceUID\": study_id,\n        \"SeriesInstanceUID\": series_id,\n        \"images\": selected_files\n    })\n\nprint(\"Usable series:\", len(samples))\n\nprint(\"\\nExample:\")\nprint(\"Study:\", samples[0][\"StudyInstanceUID\"])\nprint(\"Series:\", samples[0][\"SeriesInstanceUID\"])\nprint(\"Number of selected slices:\", len(samples[0][\"images\"]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:40:25.478127Z","iopub.execute_input":"2026-09-09T16:40:25.478494Z","iopub.status.idle":"2026-09-09T16:40:25.815203Z","shell.execute_reply.started":"2026-09-09T16:40:25.47846Z","shell.execute_reply":"2026-09-09T16:40:25.813748Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 6 — Verify selected slices\n# ============================================================\n\nimport matplotlib.pyplot as plt\n\nexample_images = samples[0][\"images\"]\n\nplt.figure(figsize=(16, 4))\n\nfor i, image_path in enumerate(example_images):\n\n    image = Image.open(image_path)\n\n    plt.subplot(1, NUM_SLICES, i + 1)\n    plt.imshow(image, cmap=\"gray\")\n    plt.axis(\"off\")\n    plt.title(f\"Slice {i+1}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:40:36.358409Z","iopub.execute_input":"2026-09-09T16:40:36.360182Z","iopub.status.idle":"2026-09-09T16:40:37.026761Z","shell.execute_reply.started":"2026-09-09T16:40:36.360104Z","shell.execute_reply":"2026-09-09T16:40:37.024371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 7 — Study-level train/validation split\n# ============================================================\n\nfrom sklearn.model_selection import train_test_split\n\n# Unique studies represented in our usable dataset\nusable_studies = list(set(\n    sample[\"StudyInstanceUID\"]\n    for sample in samples\n))\n\nprint(\"Usable studies:\", len(usable_studies))\n\ntrain_studies, val_studies = train_test_split(\n    usable_studies,\n    test_size=0.20,\n    random_state=42\n)\n\nprint(\"Training studies:\", len(train_studies))\nprint(\"Validation studies:\", len(val_studies))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:42:10.011318Z","iopub.execute_input":"2026-09-09T16:42:10.01172Z","iopub.status.idle":"2026-09-09T16:42:10.021399Z","shell.execute_reply.started":"2026-09-09T16:42:10.011685Z","shell.execute_reply":"2026-09-09T16:42:10.020099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 8 — MobileNetV2\n# ============================================================\n\nimport tensorflow as tf\nfrom tensorflow.keras.applications import MobileNetV2\nfrom tensorflow.keras.layers import (\n    Input,\n    GlobalAveragePooling2D,\n    Dense,\n    Dropout\n)\nfrom tensorflow.keras.models import Model\n\nIMG_SIZE = 224\nNUM_CLASSES = 12\n\n# Input is grayscale, so convert to 3 channels\ninputs = Input(\n    shape=(IMG_SIZE, IMG_SIZE, 1),\n    name=\"mri_image\"\n)\n\n# Convert grayscale → RGB\nx = tf.keras.layers.Concatenate()([\n    inputs,\n    inputs,\n    inputs\n])\n\n# MobileNetV2 expects ImageNet-style input\nx = tf.keras.applications.mobilenet_v2.preprocess_input(\n    x\n)\n\n# Pretrained MobileNetV2\nbase_model = MobileNetV2(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\n\n# Freeze initially\nbase_model.trainable = False\n\nx = base_model(x, training=False)\n\nx = GlobalAveragePooling2D()(x)\n\nx = Dense(256, activation=\"relu\")(x)\n\nx = Dropout(0.3)(x)\n\noutputs = Dense(\n    NUM_CLASSES,\n    activation=\"sigmoid\",\n    name=\"predictions\"\n)(x)\n\nmodel = Model(inputs, outputs)\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(\n        learning_rate=1e-4\n    ),\n    loss=\"binary_crossentropy\"\n)\n\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:42:12.676203Z","iopub.execute_input":"2026-09-09T16:42:12.676592Z","iopub.status.idle":"2026-09-09T16:42:15.375963Z","shell.execute_reply.started":"2026-09-09T16:42:12.67656Z","shell.execute_reply":"2026-09-09T16:42:15.374757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 9 — Fast MobileNetV2 feature extraction\n# ============================================================\n\nimport numpy as np\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nFEATURE_SIZE = 1280\nNUM_SLICES = 8\n\n# Feature extractor\nfeature_extractor = tf.keras.Model(\n    inputs=base_model.input,\n    outputs=base_model.output\n)\n\ndef load_image(path):\n    image = Image.open(path).convert(\"L\")\n    image = np.array(image, dtype=np.float32)\n\n    # 224 x 224\n    image = np.expand_dims(image, axis=-1)\n\n    # grayscale → RGB\n    image = np.repeat(image, 3, axis=-1)\n\n    return image\n\n\ndef extract_study_features(sample):\n\n    images = []\n\n    for path in sample[\"images\"]:\n        images.append(load_image(path))\n\n    images = np.array(images)\n\n    # MobileNetV2 preprocessing\n    images = tf.keras.applications.mobilenet_v2.preprocess_input(\n        images\n    )\n\n    # Extract features\n    features = feature_extractor.predict(\n        images,\n        verbose=0\n    )\n\n    # Average 8 slices → one study representation\n    study_feature = features.mean(axis=0)\n\n    return study_feature\n\n\nX = []\nstudy_ids = []\n\nprint(\"Extracting features...\")\n\nfor sample in tqdm(samples):\n\n    feature = extract_study_features(sample)\n\n    X.append(feature)\n    study_ids.append(sample[\"StudyInstanceUID\"])\n\nX = np.array(X)\n\nprint(\"\\nFeature shape:\", X.shape)\nprint(\"Number of samples:\", len(X))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:42:59.332467Z","iopub.execute_input":"2026-09-09T16:42:59.332864Z","iopub.status.idle":"2026-09-09T16:43:18.139411Z","shell.execute_reply.started":"2026-09-09T16:42:59.332831Z","shell.execute_reply":"2026-09-09T16:43:18.138612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 10 — Convert 7x7x1280 features → 1280 study features\n# ============================================================\n\n# Global average pooling over the 7x7 spatial dimensions\nX_series = X.mean(axis=(1, 2))\n\nprint(\"Before pooling:\", X.shape)\nprint(\"After pooling:\", X_series.shape)\n\n# Create dataframe connecting each feature to its study\nfeature_df = pd.DataFrame({\n    \"StudyInstanceUID\": study_ids\n})\n\n# Add feature columns\nfeature_columns = [f\"feature_{i}\" for i in range(1280)]\n\nfeature_data = pd.DataFrame(\n    X_series,\n    columns=feature_columns\n)\n\nfeature_df = pd.concat(\n    [feature_df, feature_data],\n    axis=1\n)\n\n# Some studies have multiple qualifying series.\n# Average their features to obtain ONE representation per study.\nstudy_features = (\n    feature_df\n    .groupby(\"StudyInstanceUID\")[feature_columns]\n    .mean()\n    .reset_index()\n)\n\nprint(\"\\nStudy-level feature shape:\")\nprint(study_features.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:45:40.381121Z","iopub.execute_input":"2026-09-09T16:45:40.381508Z","iopub.status.idle":"2026-09-09T16:45:40.407853Z","shell.execute_reply.started":"2026-09-09T16:45:40.381468Z","shell.execute_reply":"2026-09-09T16:45:40.406427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 11 — Attach official labels\n# ============================================================\n\nlabels = train[\n    [\"StudyInstanceUID\"] + target_columns\n].copy()\n\n# Keep only studies represented in our MRI features\nstudy_data = study_features.merge(\n    labels,\n    on=\"StudyInstanceUID\",\n    how=\"inner\"\n)\n\nprint(\"Final studies:\", len(study_data))\nprint(\"Final columns:\", study_data.shape[1])\n\ndisplay(\n    study_data[\n        [\"StudyInstanceUID\"] + target_columns\n    ].head()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:45:58.946647Z","iopub.execute_input":"2026-09-09T16:45:58.947046Z","iopub.status.idle":"2026-09-09T16:45:58.97835Z","shell.execute_reply.started":"2026-09-09T16:45:58.947012Z","shell.execute_reply":"2026-09-09T16:45:58.976516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 12 — Study-level 12-label classifier\n# ============================================================\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nimport tensorflow as tf\nimport numpy as np\n\n# ------------------------------------------------------------\n# Prepare X and y\n# ------------------------------------------------------------\n\nX_final = study_data[feature_columns].values.astype(np.float32)\n\ny_final = study_data[target_columns].values.astype(np.float32)\n\n# Find the study IDs\nstudy_ids_final = study_data[\"StudyInstanceUID\"].values\n\n# ------------------------------------------------------------\n# Use the SAME study split we created earlier\n# ------------------------------------------------------------\n\ntrain_mask = np.isin(\n    study_ids_final,\n    train_studies\n)\n\nval_mask = np.isin(\n    study_ids_final,\n    val_studies\n)\n\nX_train = X_final[train_mask]\nX_val = X_final[val_mask]\n\ny_train = y_final[train_mask]\ny_val = y_final[val_mask]\n\nprint(\"X_train:\", X_train.shape)\nprint(\"X_val:\", X_val.shape)\nprint(\"y_train:\", y_train.shape)\nprint(\"y_val:\", y_val.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:46:10.044215Z","iopub.execute_input":"2026-09-09T16:46:10.044611Z","iopub.status.idle":"2026-09-09T16:46:10.059248Z","shell.execute_reply.started":"2026-09-09T16:46:10.044578Z","shell.execute_reply":"2026-09-09T16:46:10.057905Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 13 — Feature scaling\n# ============================================================\n\nscaler = StandardScaler()\n\nX_train_scaled = scaler.fit_transform(X_train)\nX_val_scaled = scaler.transform(X_val)\n\nprint(\"Scaled training data:\", X_train_scaled.shape)\nprint(\"Scaled validation data:\", X_val_scaled.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:46:22.400209Z","iopub.execute_input":"2026-09-09T16:46:22.400601Z","iopub.status.idle":"2026-09-09T16:46:22.411591Z","shell.execute_reply.started":"2026-09-09T16:46:22.400563Z","shell.execute_reply":"2026-09-09T16:46:22.410291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 14 — Train classifier\n# ============================================================\n\nclassifier = tf.keras.Sequential([\n    tf.keras.layers.Input(shape=(1280,)),\n\n    tf.keras.layers.Dense(\n        256,\n        activation=\"relu\"\n    ),\n\n    tf.keras.layers.Dropout(0.4),\n\n    tf.keras.layers.Dense(\n        128,\n        activation=\"relu\"\n    ),\n\n    tf.keras.layers.Dropout(0.3),\n\n    tf.keras.layers.Dense(\n        12,\n        activation=\"sigmoid\"\n    )\n])\n\nclassifier.compile(\n    optimizer=tf.keras.optimizers.Adam(\n        learning_rate=1e-3\n    ),\n    loss=\"binary_crossentropy\"\n)\n\nclassifier.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:46:33.078967Z","iopub.execute_input":"2026-09-09T16:46:33.079413Z","iopub.status.idle":"2026-09-09T16:46:33.148748Z","shell.execute_reply.started":"2026-09-09T16:46:33.079375Z","shell.execute_reply":"2026-09-09T16:46:33.148068Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 15 — Training\n# ============================================================\n\nhistory = classifier.fit(\n    X_train_scaled,\n    y_train,\n    validation_data=(\n        X_val_scaled,\n        y_val\n    ),\n    epochs=30,\n    batch_size=8,\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:46:49.600502Z","iopub.execute_input":"2026-09-09T16:46:49.600872Z","iopub.status.idle":"2026-09-09T16:46:54.026806Z","shell.execute_reply.started":"2026-09-09T16:46:49.60084Z","shell.execute_reply":"2026-09-09T16:46:54.025671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 16 — Calculate ROC-AUC\n# ============================================================\n\nfrom sklearn.metrics import roc_auc_score\nimport numpy as np\nimport pandas as pd\n\n# Predictions\nval_predictions = classifier.predict(\n    X_val_scaled,\n    verbose=0\n)\n\nprint(\"Prediction shape:\", val_predictions.shape)\n\nauc_results = {}\n\nfor i, target in enumerate(target_columns):\n\n    y_true = y_val[:, i]\n    y_pred = val_predictions[:, i]\n\n    # AUC cannot be calculated if validation set\n    # contains only one class\n    if len(np.unique(y_true)) < 2:\n        auc_results[target] = np.nan\n    else:\n        auc_results[target] = roc_auc_score(\n            y_true,\n            y_pred\n        )\n\nauc_df = pd.DataFrame(\n    list(auc_results.items()),\n    columns=[\"Abnormality\", \"ROC-AUC\"]\n)\n\ndisplay(auc_df)\n\nmacro_auc = auc_df[\"ROC-AUC\"].mean()\n\nprint(\"\\n================================\")\nprint(f\"Macro ROC-AUC: {macro_auc:.4f}\")\nprint(\"================================\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:47:39.430254Z","iopub.execute_input":"2026-09-09T16:47:39.430674Z","iopub.status.idle":"2026-09-09T16:47:39.574313Z","shell.execute_reply.started":"2026-09-09T16:47:39.430639Z","shell.execute_reply":"2026-09-09T16:47:39.572698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 17 — Select one MRI series per plane for each study\n# ============================================================\n\nimport pandas as pd\nimport numpy as np\n\n# Use fluid-sensitive + fat-suppressed series first\npreferred = labeled_series[\n    (labeled_series[\"Fluid_Sensitive\"] == 1) &\n    (labeled_series[\"Fat_Suppression\"] == 1)\n].copy()\n\n# If a study has multiple series in the same plane,\n# choose the one with the largest number of slices.\nseries_info = []\n\nfor _, row in preferred.iterrows():\n\n    image_files = get_image_files(\n        row[\"StudyInstanceUID\"],\n        row[\"SeriesInstanceUID\"]\n    )\n\n    series_info.append({\n        \"StudyInstanceUID\": row[\"StudyInstanceUID\"],\n        \"SeriesInstanceUID\": row[\"SeriesInstanceUID\"],\n        \"Anatomical_Plane\": row[\"Anatomical_Plane\"],\n        \"num_slices\": len(image_files)\n    })\n\nseries_info = pd.DataFrame(series_info)\n\n# Remove unusable series\nseries_info = series_info[\n    series_info[\"num_slices\"] >= 8\n].copy()\n\n# Select the largest series for each study + plane\nbest_series = (\n    series_info\n    .sort_values(\"num_slices\", ascending=False)\n    .groupby(\n        [\"StudyInstanceUID\", \"Anatomical_Plane\"],\n        as_index=False\n    )\n    .first()\n)\n\nprint(\"Selected series:\", len(best_series))\n\nprint(\"\\nSeries by plane:\")\nprint(best_series[\"Anatomical_Plane\"].value_counts())\n\ndisplay(best_series.head(15))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:48:59.181123Z","iopub.execute_input":"2026-09-09T16:48:59.181523Z","iopub.status.idle":"2026-09-09T16:49:00.04782Z","shell.execute_reply.started":"2026-09-09T16:48:59.181489Z","shell.execute_reply":"2026-09-09T16:49:00.046412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 FAST — Multi-plane feature extraction\n# ============================================================\n\nNUM_SLICES_FAST = 4\n\ndef select_slices_fast(image_files, num_slices=4):\n\n    if len(image_files) <= num_slices:\n        return image_files\n\n    indices = np.linspace(\n        0,\n        len(image_files) - 1,\n        num_slices\n    ).astype(int)\n\n    return [image_files[i] for i in indices]\n\n\ndef extract_series_feature_fast(study_id, series_id):\n\n    image_files = get_image_files(\n        study_id,\n        series_id\n    )\n\n    image_files = select_slices_fast(\n        image_files,\n        NUM_SLICES_FAST\n    )\n\n    images = np.array(\n        [load_image(p) for p in image_files],\n        dtype=np.float32\n    )\n\n    images = tf.keras.applications.mobilenet_v2.preprocess_input(\n        images\n    )\n\n    features = feature_extractor.predict(\n        images,\n        verbose=0\n    )\n\n    # Average slices + 7x7 spatial dimensions\n    return features.mean(axis=(0, 1, 2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:51:17.355838Z","iopub.execute_input":"2026-09-09T16:51:17.356187Z","iopub.status.idle":"2026-09-09T16:51:17.363352Z","shell.execute_reply.started":"2026-09-09T16:51:17.356156Z","shell.execute_reply":"2026-09-09T16:51:17.362416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 19 FAST — Extract features\n# ============================================================\n\nfrom tqdm.auto import tqdm\n\nmulti_features = []\n\nstudy_list = best_series[\"StudyInstanceUID\"].unique()\n\nfor study_id in tqdm(\n    study_list,\n    desc=\"Processing studies\"\n):\n\n    result = {\n        \"StudyInstanceUID\": study_id\n    }\n\n    study_rows = best_series[\n        best_series[\"StudyInstanceUID\"] == study_id\n    ]\n\n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n        rows = study_rows[\n            study_rows[\"Anatomical_Plane\"] == plane\n        ]\n\n        if len(rows) == 0:\n            continue\n\n        row = rows.iloc[0]\n\n        feature = extract_series_feature_fast(\n            row[\"StudyInstanceUID\"],\n            row[\"SeriesInstanceUID\"]\n        )\n\n        for i, value in enumerate(feature):\n            result[f\"{plane}_{i}\"] = value\n\n    multi_features.append(result)\n\nmulti_features = pd.DataFrame(multi_features)\n\nprint(\"\\nFinished!\")\nprint(\"Shape:\", multi_features.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:51:29.255592Z","iopub.execute_input":"2026-09-09T16:51:29.256009Z","iopub.status.idle":"2026-09-09T16:52:00.393944Z","shell.execute_reply.started":"2026-09-09T16:51:29.255955Z","shell.execute_reply":"2026-09-09T16:52:00.392613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 20 — Prepare Multi-Series Features\n# ============================================\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import roc_auc_score\n\n# --------------------------------------------\n# 1. Separate IDs and features\n# --------------------------------------------\n\nfeature_columns = [\n    col for col in multi_features.columns\n    if col != \"StudyInstanceUID\"\n]\n\nX_multi = multi_features[feature_columns].copy()\n\nprint(\"Feature matrix shape:\", X_multi.shape)\nprint(\"Number of features:\", len(feature_columns))\n\n\n# --------------------------------------------\n# 2. Handle missing planes\n# --------------------------------------------\n\nX_multi = X_multi.fillna(0)\n\n# Convert to numpy\nX_multi = X_multi.values.astype(np.float32)\n\n\n# --------------------------------------------\n# 3. Get labels\n# --------------------------------------------\n\nmulti_labels = train[\n    train[\"StudyInstanceUID\"].isin(\n        multi_features[\"StudyInstanceUID\"]\n    )\n].copy()\n\n# Make sure ordering matches multi_features\nmulti_labels = (\n    multi_features[[\"StudyInstanceUID\"]]\n    .merge(\n        train[[\"StudyInstanceUID\"] + target_columns],\n        on=\"StudyInstanceUID\",\n        how=\"left\"\n    )\n)\n\ny_multi = multi_labels[target_columns].values.astype(np.float32)\n\nprint(\"X shape:\", X_multi.shape)\nprint(\"Y shape:\", y_multi.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:52:38.335633Z","iopub.execute_input":"2026-09-09T16:52:38.33602Z","iopub.status.idle":"2026-09-09T16:52:38.363042Z","shell.execute_reply.started":"2026-09-09T16:52:38.33597Z","shell.execute_reply":"2026-09-09T16:52:38.361509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 21 — Train / Validation Split\n# ============================================\n\nstudy_ids_multi = multi_features[\"StudyInstanceUID\"].values\n\ntrain_studies_multi, val_studies_multi = train_test_split(\n    study_ids_multi,\n    test_size=0.20,\n    random_state=42\n)\n\ntrain_mask_multi = np.isin(\n    study_ids_multi,\n    train_studies_multi\n)\n\nval_mask_multi = np.isin(\n    study_ids_multi,\n    val_studies_multi\n)\n\nX_train_multi = X_multi[train_mask_multi]\nX_val_multi = X_multi[val_mask_multi]\n\ny_train_multi = y_multi[train_mask_multi]\ny_val_multi = y_multi[val_mask_multi]\n\nprint(\"Training studies:\", len(train_studies_multi))\nprint(\"Validation studies:\", len(val_studies_multi))\n\nprint(\"X_train:\", X_train_multi.shape)\nprint(\"X_val:\", X_val_multi.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:52:51.17955Z","iopub.execute_input":"2026-09-09T16:52:51.179952Z","iopub.status.idle":"2026-09-09T16:52:51.193121Z","shell.execute_reply.started":"2026-09-09T16:52:51.179918Z","shell.execute_reply":"2026-09-09T16:52:51.190804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 22 — Standard Scaling\n# ============================================\n\nscaler_multi = StandardScaler()\n\nX_train_multi_scaled = scaler_multi.fit_transform(\n    X_train_multi\n)\n\nX_val_multi_scaled = scaler_multi.transform(\n    X_val_multi\n)\n\nprint(\"Scaled training shape:\", X_train_multi_scaled.shape)\nprint(\"Scaled validation shape:\", X_val_multi_scaled.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:53:02.262644Z","iopub.execute_input":"2026-09-09T16:53:02.263055Z","iopub.status.idle":"2026-09-09T16:53:02.27388Z","shell.execute_reply.started":"2026-09-09T16:53:02.263021Z","shell.execute_reply":"2026-09-09T16:53:02.272388Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 23 — Multi-Series Classifier\n# ============================================\n\nimport tensorflow as tf\n\nmulti_classifier = tf.keras.Sequential([\n    \n    tf.keras.layers.Input(shape=(3840,)),\n    \n    tf.keras.layers.Dense(\n        256,\n        activation=\"relu\"\n    ),\n    \n    tf.keras.layers.Dropout(0.5),\n    \n    tf.keras.layers.Dense(\n        128,\n        activation=\"relu\"\n    ),\n    \n    tf.keras.layers.Dropout(0.4),\n    \n    tf.keras.layers.Dense(\n        12,\n        activation=\"sigmoid\"\n    )\n])\n\nmulti_classifier.compile(\n    optimizer=tf.keras.optimizers.Adam(\n        learning_rate=1e-3\n    ),\n    loss=\"binary_crossentropy\"\n)\n\nmulti_classifier.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:53:13.465272Z","iopub.execute_input":"2026-09-09T16:53:13.465641Z","iopub.status.idle":"2026-09-09T16:53:13.528682Z","shell.execute_reply.started":"2026-09-09T16:53:13.465609Z","shell.execute_reply":"2026-09-09T16:53:13.528059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 24 — Train with Early Stopping\n# ============================================\n\nearly_stopping = tf.keras.callbacks.EarlyStopping(\n    monitor=\"val_loss\",\n    patience=5,\n    restore_best_weights=True\n)\n\nhistory_multi = multi_classifier.fit(\n    X_train_multi_scaled,\n    y_train_multi,\n    validation_data=(\n        X_val_multi_scaled,\n        y_val_multi\n    ),\n    epochs=50,\n    batch_size=8,\n    callbacks=[early_stopping],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:53:23.959644Z","iopub.execute_input":"2026-09-09T16:53:23.960091Z","iopub.status.idle":"2026-09-09T16:53:26.250151Z","shell.execute_reply.started":"2026-09-09T16:53:23.960051Z","shell.execute_reply":"2026-09-09T16:53:26.248606Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 25 — Evaluate Multi-Series Model\n# ============================================\n\ny_pred_multi = multi_classifier.predict(\n    X_val_multi_scaled,\n    verbose=0\n)\n\nprint(\"Prediction shape:\", y_pred_multi.shape)\n\nauc_scores_multi = {}\n\nfor i, target in enumerate(target_columns):\n\n    y_true = y_val_multi[:, i]\n    y_pred = y_pred_multi[:, i]\n\n    # AUC requires both classes to be present\n    if len(np.unique(y_true)) < 2:\n        auc_scores_multi[target] = np.nan\n    else:\n        auc_scores_multi[target] = roc_auc_score(\n            y_true,\n            y_pred\n        )\n\nprint(\"\\nMulti-Series AUC Results:\\n\")\n\nfor target, score in auc_scores_multi.items():\n    print(\n        f\"{target:20s}: \"\n        f\"{score:.4f}\" if not np.isnan(score)\n        else f\"{target:20s}: NaN\"\n    )\n\nvalid_auc = [\n    score\n    for score in auc_scores_multi.values()\n    if not np.isnan(score)\n]\n\nmacro_auc_multi = np.mean(valid_auc)\n\nprint(\"\\n================================\")\nprint(f\"Macro AUC: {macro_auc_multi:.4f}\")\nprint(\"================================\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:53:51.865803Z","iopub.execute_input":"2026-09-09T16:53:51.866222Z","iopub.status.idle":"2026-09-09T16:53:51.997746Z","shell.execute_reply.started":"2026-09-09T16:53:51.866188Z","shell.execute_reply":"2026-09-09T16:53:51.996124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 26 — Analyze All Training Reports\n# ============================================\n\nimport pandas as pd\nimport numpy as np\n\nprint(\"Total studies:\", len(train))\n\n# How many reports are available?\nprint(\"Reports available:\", train[\"Report\"].notna().sum())\nprint(\"Reports missing:\", train[\"Report\"].isna().sum())\n\n# Officially labeled vs unlabeled\nlabeled_mask = train[target_columns].notna().all(axis=1)\n\nprint(\"\\nOfficially labeled studies:\", labeled_mask.sum())\nprint(\"Reports-only studies:\", (~labeled_mask).sum())\n\n# Report lengths\ntrain[\"report_length\"] = (\n    train[\"Report\"]\n    .fillna(\"\")\n    .astype(str)\n    .str.len()\n)\n\nprint(\"\\nReport length statistics:\")\nprint(train[\"report_length\"].describe())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:55:54.609671Z","iopub.execute_input":"2026-09-09T16:55:54.610076Z","iopub.status.idle":"2026-09-09T16:55:54.633757Z","shell.execute_reply.started":"2026-09-09T16:55:54.610043Z","shell.execute_reply":"2026-09-09T16:55:54.632529Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 27 — Look at Reports-Only Studies\n# ============================================\n\nunlabeled_reports = train[\n    ~labeled_mask\n][[\"StudyInstanceUID\", \"Report\", \"report_length\"]].copy()\n\ndisplay(\n    unlabeled_reports\n    .sort_values(\"report_length\", ascending=False)\n    .head(20)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T16:59:54.03564Z","iopub.execute_input":"2026-09-09T16:59:54.03611Z","iopub.status.idle":"2026-09-09T16:59:54.052122Z","shell.execute_reply.started":"2026-09-09T16:59:54.036057Z","shell.execute_reply":"2026-09-09T16:59:54.051425Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================\n# STEP 28 — Inspect Different Report Styles\n# ============================================\n\nsample_reports = unlabeled_reports.sample(\n    min(30, len(unlabeled_reports)),\n    random_state=42\n)\n\nfor i, (_, row) in enumerate(sample_reports.iterrows(), 1):\n    print(\"=\" * 80)\n    print(f\"REPORT {i}\")\n    print(\"=\" * 80)\n    print(row[\"Report\"])\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:00:17.512195Z","iopub.execute_input":"2026-09-09T17:00:17.512577Z","iopub.status.idle":"2026-09-09T17:00:17.52366Z","shell.execute_reply.started":"2026-09-09T17:00:17.512545Z","shell.execute_reply":"2026-09-09T17:00:17.522224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\nimport pandas as pd\nimport numpy as np\n\ndef weak_acl_label(report):\n    \"\"\"\n    Returns:\n        1 = high-confidence ACL abnormality\n        0 = high-confidence ACL normal\n       -1 = uncertain / do not use\n    \"\"\"\n\n    text = str(report).lower()\n\n    # ----------------------------------------\n    # Normalize common ACL names\n    # ----------------------------------------\n\n    replacements = {\n        \"anterior cruciate ligament\": \"acl\",\n        \"ligamentum cruciatum anterius\": \"acl\",\n        \"lca\": \"acl\",\n        \"vkb\": \"acl\",\n        \"ligamento cruzado anterior\": \"acl\",\n        \"ligament croisé antérieur\": \"acl\",\n        \"ligament croise anterieur\": \"acl\",\n        \"ön çapraz bağ\": \"acl\",\n        \"on capraz bag\": \"acl\",\n        \"предна кръстна връзка\": \"acl\",\n        \"prednji križni ligament\": \"acl\",\n        \"prednji krizni ligament\": \"acl\",\n        \"πρόσθιος χιαστός σύνδεσμος\": \"acl\",\n    }\n\n    for old, new in replacements.items():\n        text = text.replace(old, new)\n\n    # ----------------------------------------\n    # HIGH-CONFIDENCE NEGATIVE\n    # ----------------------------------------\n\n    negative_patterns = [\n\n        r'\\bacl\\b[^.]{0,100}\\bnormal\\b',\n        r'\\bnormal\\b[^.]{0,100}\\bacl\\b',\n\n        r'\\bacl\\b[^.]{0,100}\\bintact\\b',\n        r'\\bintact\\b[^.]{0,100}\\bacl\\b',\n\n        r'\\bacl\\b[^.]{0,100}\\bpreserved\\b',\n        r'\\bpreserved\\b[^.]{0,100}\\bacl\\b',\n\n        r'\\bacl\\b[^.]{0,100}\\bno\\s+(?:evidence\\s+of\\s+)?(?:tear|rupture|disruption)\\b',\n\n        r'\\bno\\s+(?:evidence\\s+of\\s+)?(?:tear|rupture|disruption)\\b[^.]{0,100}\\bacl\\b',\n\n        r'\\bacl\\b[^.]{0,100}\\bwithout\\s+(?:tear|rupture)\\b',\n    ]\n\n    # ----------------------------------------\n    # HIGH-CONFIDENCE POSITIVE\n    # ----------------------------------------\n\n    positive_patterns = [\n\n        # English\n        r'\\bacl\\b[^.]{0,120}\\b(?:complete|full[- ]thickness|full[- ]width)\\b[^.]{0,60}\\b(?:tear|rupture|disruption)\\b',\n\n        r'\\bacl\\b[^.]{0,120}\\b(?:tear|rupture|ruptured|disruption)\\b',\n\n        r'\\b(?:tear|rupture|ruptured|disruption)\\b[^.]{0,120}\\bacl\\b',\n\n        # Spanish / Portuguese style \"rotura\"\n        r'\\bacl\\b[^.]{0,120}\\brotura\\b',\n\n        r'\\brotura\\b[^.]{0,120}\\bacl\\b',\n\n        # Turkish\n        r'\\bacl\\b[^.]{0,120}\\b(?:yırtık|yirtik|rüptür|ruptur|parsiyel)\\b',\n\n        # Croatian / Slavic\n        r'\\bacl\\b[^.]{0,120}\\b(?:ruptura|ruptur|razdor)\\b',\n\n        # Greek transliteration / terminology\n        r'\\bacl\\b[^.]{0,120}\\b(?:ρήξη|ολική ρήξη|μερική ρήξη)\\b',\n\n        # Explicit discontinuity\n        r'\\bacl\\b[^.]{0,120}\\b(?:discontinuity|disruption)\\b',\n    ]\n\n    # ----------------------------------------\n    # Check NEGATIVE first\n    # ----------------------------------------\n\n    for pattern in negative_patterns:\n        if re.search(pattern, text):\n            return 0\n\n    # ----------------------------------------\n    # Check POSITIVE\n    # ----------------------------------------\n\n    for pattern in positive_patterns:\n        if re.search(pattern, text):\n            return 1\n\n    # ----------------------------------------\n    # Otherwise uncertain\n    # ----------------------------------------\n\n    return -1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:04:48.867514Z","iopub.execute_input":"2026-09-09T17:04:48.867856Z","iopub.status.idle":"2026-09-09T17:04:48.876158Z","shell.execute_reply.started":"2026-09-09T17:04:48.867832Z","shell.execute_reply":"2026-09-09T17:04:48.875266Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train[\"ACL_weak\"] = train[\"Report\"].apply(\n    weak_acl_label\n)\n\nprint(\n    train[\"ACL_weak\"]\n    .value_counts()\n    .sort_index()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:02:59.536668Z","iopub.execute_input":"2026-09-09T17:02:59.537054Z","iopub.status.idle":"2026-09-09T17:03:00.282143Z","shell.execute_reply.started":"2026-09-09T17:02:59.536983Z","shell.execute_reply":"2026-09-09T17:03:00.281264Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for label in [1, 0, -1]:\n\n    print(\"\\n\")\n    print(\"=\" * 80)\n    print(\"WEAK LABEL:\", label)\n    print(\"=\" * 80)\n\n    examples = train[\n        train[\"ACL_weak\"] == label\n    ][\"Report\"].head(5)\n\n    for i, report in enumerate(examples, 1):\n        print(f\"\\n--- Example {i} ---\")\n        print(report[:1000])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:03:11.585571Z","iopub.execute_input":"2026-09-09T17:03:11.5859Z","iopub.status.idle":"2026-09-09T17:03:11.596899Z","shell.execute_reply.started":"2026-09-09T17:03:11.585873Z","shell.execute_reply":"2026-09-09T17:03:11.596068Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Only official ACL-labelled studies\nofficial_acl = train[\n    train[\"ACL\"].notna()\n].copy()\n\nofficial_acl[\"ACL_weak\"] = official_acl[\"Report\"].apply(\n    weak_acl_label\n)\n\n# Only confident weak labels\ncomparison = official_acl[\n    official_acl[\"ACL_weak\"] != -1\n]\n\nprint(\"Official ACL studies:\", len(official_acl))\nprint(\"Confident weak labels:\", len(comparison))\n\nprint(\"\\nConfusion matrix:\")\nprint(\n    pd.crosstab(\n        comparison[\"ACL\"],\n        comparison[\"ACL_weak\"],\n        rownames=[\"Official ACL\"],\n        colnames=[\"Weak ACL\"]\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:03:35.029678Z","iopub.execute_input":"2026-09-09T17:03:35.03012Z","iopub.status.idle":"2026-09-09T17:03:35.079511Z","shell.execute_reply.started":"2026-09-09T17:03:35.030088Z","shell.execute_reply":"2026-09-09T17:03:35.07883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import (\n    precision_score,\n    recall_score,\n    f1_score\n)\n\nif len(comparison) > 0:\n\n    y_true = comparison[\"ACL\"].astype(int)\n    y_pred = comparison[\"ACL_weak\"].astype(int)\n\n    print(\"Precision:\",\n          precision_score(y_true, y_pred))\n\n    print(\"Recall:\",\n          recall_score(y_true, y_pred))\n\n    print(\"F1:\",\n          f1_score(y_true, y_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:03:47.226047Z","iopub.execute_input":"2026-09-09T17:03:47.226445Z","iopub.status.idle":"2026-09-09T17:03:47.243965Z","shell.execute_reply.started":"2026-09-09T17:03:47.226413Z","shell.execute_reply":"2026-09-09T17:03:47.242436Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"errors = comparison[\n    comparison[\"ACL\"] != comparison[\"ACL_weak\"]\n].copy()\n\nprint(\"Number of disagreements:\", len(errors))\n\nfor i, (_, row) in enumerate(errors.iterrows(), 1):\n    print(\"=\" * 100)\n    print(f\"ERROR {i}\")\n    print(\"Official ACL :\", int(row[\"ACL\"]))\n    print(\"Weak ACL     :\", int(row[\"ACL_weak\"]))\n    print(\"\\nREPORT:\")\n    print(row[\"Report\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:05:59.693279Z","iopub.execute_input":"2026-09-09T17:05:59.693711Z","iopub.status.idle":"2026-09-09T17:05:59.703413Z","shell.execute_reply.started":"2026-09-09T17:05:59.693667Z","shell.execute_reply":"2026-09-09T17:05:59.702214Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import re\n\ndef weak_acl_label_v2(report):\n    \"\"\"\n    ACL weak labeling.\n\n    Returns:\n        1  = strong ACL abnormality\n        0  = strong ACL normal\n       -1  = uncertain\n    \"\"\"\n\n    text = str(report).lower()\n\n    # --------------------------------------------------\n    # 1. Normalize ACL terminology\n    # --------------------------------------------------\n\n    replacements = {\n        \"anterior cruciate ligament\": \"acl\",\n        \"ligamentum cruciatum anterius\": \"acl\",\n\n        \"ligamento cruzado anterior\": \"acl\",\n        \"ligament croisé antérieur\": \"acl\",\n        \"ligament croise anterieur\": \"acl\",\n\n        \"ön çapraz bağ\": \"acl\",\n        \"on capraz bag\": \"acl\",\n\n        \"предна кръстна връзка\": \"acl\",\n        \"prednji križni ligament\": \"acl\",\n        \"prednji krizni ligament\": \"acl\",\n\n        \"πρόσθιος χιαστός σύνδεσμος\": \"acl\",\n    }\n\n    for old, new in replacements.items():\n        text = text.replace(old, new)\n\n    # Normalize whitespace\n    text = re.sub(r\"\\s+\", \" \", text).strip()\n\n    # --------------------------------------------------\n    # 2. Strong NEGATIVE evidence\n    # --------------------------------------------------\n\n    strong_negative = [\n        r\"\\bacl\\b.{0,120}\\bnormal\\b\",\n        r\"\\bnormal\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bintact\\b\",\n        r\"\\bintact\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bpreserved\\b\",\n        r\"\\bpreserved\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bwithin normal\\b\",\n        r\"\\bwithin normal\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bno evidence of tear\\b\",\n        r\"\\bacl\\b.{0,120}\\bno evidence of rupture\\b\",\n\n        r\"\\bno evidence of tear\\b.{0,120}\\bacl\\b\",\n        r\"\\bno evidence of rupture\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bwithout tear\\b\",\n        r\"\\bacl\\b.{0,120}\\bwithout rupture\\b\",\n    ]\n\n    # --------------------------------------------------\n    # 3. Strong POSITIVE evidence\n    # --------------------------------------------------\n\n    strong_positive = [\n\n        # Complete tear / rupture\n        r\"\\bacl\\b.{0,120}\\bcomplete tear\\b\",\n        r\"\\bcomplete tear\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bcomplete rupture\\b\",\n        r\"\\bcomplete rupture\\b.{0,120}\\bacl\\b\",\n\n        # Full thickness / full width\n        r\"\\bacl\\b.{0,120}\\bfull[- ]thickness tear\\b\",\n        r\"\\bfull[- ]thickness tear\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bfull[- ]width tear\\b\",\n        r\"\\bfull[- ]width tear\\b.{0,120}\\bacl\\b\",\n\n        # Explicit rupture\n        r\"\\bacl\\b.{0,120}\\bis ruptured\\b\",\n        r\"\\bacl\\b.{0,120}\\bruptured\\b\",\n\n        r\"\\bruptured\\b.{0,120}\\bacl\\b\",\n\n        # Explicit disruption\n        r\"\\bacl\\b.{0,120}\\bdisruption\\b\",\n        r\"\\bdisruption\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bdisrupted\\b\",\n        r\"\\bdisrupted\\b.{0,120}\\bacl\\b\",\n\n        # Loss of continuity\n        r\"\\bacl\\b.{0,120}\\bloss of continuity\\b\",\n        r\"\\bacl\\b.{0,120}\\bcontinuity is lost\\b\",\n\n        # Clear tear\n        r\"\\bacl\\b.{0,100}\\btear\\b\",\n        r\"\\btear\\b.{0,100}\\bacl\\b\",\n\n        # Multilingual\n        r\"\\bacl\\b.{0,120}\\brotura\\b\",\n        r\"\\brotura\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\bruptura\\b\",\n        r\"\\bruptura\\b.{0,120}\\bacl\\b\",\n\n        r\"\\bacl\\b.{0,120}\\brüptür\\b\",\n        r\"\\bacl\\b.{0,120}\\bruptur\\b\",\n    ]\n\n    # --------------------------------------------------\n    # 4. First check strong positive/negative\n    # --------------------------------------------------\n\n    negative_found = any(\n        re.search(pattern, text)\n        for pattern in strong_negative\n    )\n\n    positive_found = any(\n        re.search(pattern, text)\n        for pattern in strong_positive\n    )\n\n    # If BOTH occur, don't trust the parser.\n    # Example: normal ACL in one sentence + tear in another.\n    if positive_found and negative_found:\n        return -1\n\n    if positive_found:\n        return 1\n\n    if negative_found:\n        return 0\n\n    # --------------------------------------------------\n    # 5. Everything else = uncertain\n    # --------------------------------------------------\n\n    return -1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:07:07.623078Z","iopub.execute_input":"2026-09-09T17:07:07.623434Z","iopub.status.idle":"2026-09-09T17:07:07.63383Z","shell.execute_reply.started":"2026-09-09T17:07:07.623406Z","shell.execute_reply":"2026-09-09T17:07:07.632781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train[\"ACL_weak_v2\"] = train[\"Report\"].apply(\n    weak_acl_label_v2\n)\n\nprint(train[\"ACL_weak_v2\"].value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:07:21.931592Z","iopub.execute_input":"2026-09-09T17:07:21.931959Z","iopub.status.idle":"2026-09-09T17:07:23.976526Z","shell.execute_reply.started":"2026-09-09T17:07:21.931932Z","shell.execute_reply":"2026-09-09T17:07:23.975563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"official_acl = train[train[\"ACL\"].notna()].copy()\n\nofficial_acl[\"ACL_weak_v2\"] = official_acl[\"Report\"].apply(\n    weak_acl_label_v2\n)\n\ncomparison_v2 = official_acl[\n    official_acl[\"ACL_weak_v2\"] != -1\n]\n\nprint(\"Official ACL studies:\", len(official_acl))\nprint(\"Confident weak labels:\", len(comparison_v2))\n\nprint(\"\\nConfusion Matrix:\")\n\nprint(\n    pd.crosstab(\n        comparison_v2[\"ACL\"],\n        comparison_v2[\"ACL_weak_v2\"],\n        rownames=[\"Official ACL\"],\n        colnames=[\"Weak ACL\"]\n    )\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:07:35.611507Z","iopub.execute_input":"2026-09-09T17:07:35.611869Z","iopub.status.idle":"2026-09-09T17:07:35.661066Z","shell.execute_reply.started":"2026-09-09T17:07:35.611841Z","shell.execute_reply":"2026-09-09T17:07:35.659708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import (\n    precision_score,\n    recall_score,\n    f1_score\n)\n\ny_true = comparison_v2[\"ACL\"].astype(int)\ny_pred = comparison_v2[\"ACL_weak_v2\"].astype(int)\n\nprint(\"Precision:\", precision_score(y_true, y_pred))\nprint(\"Recall:\", recall_score(y_true, y_pred))\nprint(\"F1:\", f1_score(y_true, y_pred))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:07:48.394934Z","iopub.execute_input":"2026-09-09T17:07:48.395356Z","iopub.status.idle":"2026-09-09T17:07:48.412196Z","shell.execute_reply.started":"2026-09-09T17:07:48.395327Z","shell.execute_reply":"2026-09-09T17:07:48.411221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\nfrom sklearn.model_selection import KFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import roc_auc_score\n\ntarget_columns = [\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# Only the 58 studies with all official labels\nlabeled_data = train[\n    train[target_columns].notna().all(axis=1)\n].copy()\n\nprint(\"Officially labeled studies:\", len(labeled_data))\n\ny = labeled_data[target_columns].astype(np.float32).values\n\nprint(\"X labels shape:\", y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:09:30.075176Z","iopub.execute_input":"2026-09-09T17:09:30.0755Z","iopub.status.idle":"2026-09-09T17:09:30.087861Z","shell.execute_reply.started":"2026-09-09T17:09:30.075474Z","shell.execute_reply":"2026-09-09T17:09:30.0863Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Align single-series features to the 58 labeled studies\n\nsingle_feature_table = study_features.copy()\n\nsingle_feature_table = (\n    labeled_data[['StudyInstanceUID']]\n    .merge(\n        single_feature_table,\n        on='StudyInstanceUID',\n        how='left'\n    )\n)\n\nsingle_feature_columns = [\n    c for c in single_feature_table.columns\n    if c != 'StudyInstanceUID'\n]\n\nX_single = single_feature_table[\n    single_feature_columns\n].values.astype(np.float32)\n\nprint(\"Single-series feature shape:\", X_single.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:09:45.82176Z","iopub.execute_input":"2026-09-09T17:09:45.822184Z","iopub.status.idle":"2026-09-09T17:09:45.837811Z","shell.execute_reply.started":"2026-09-09T17:09:45.822145Z","shell.execute_reply":"2026-09-09T17:09:45.836848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multi_feature_table = multi_features.copy()\n\nmulti_feature_table = (\n    labeled_data[['StudyInstanceUID']]\n    .merge(\n        multi_feature_table,\n        on='StudyInstanceUID',\n        how='left'\n    )\n)\n\nmulti_feature_columns = [\n    c for c in multi_feature_table.columns\n    if c != 'StudyInstanceUID'\n]\n\nX_multi = multi_feature_table[\n    multi_feature_columns\n].values.astype(np.float32)\n\n# Missing plane features become zero\nX_multi = np.nan_to_num(\n    X_multi,\n    nan=0.0,\n    posinf=0.0,\n    neginf=0.0\n)\n\nprint(\"Multi-series feature shape:\", X_multi.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:09:56.129765Z","iopub.execute_input":"2026-09-09T17:09:56.130183Z","iopub.status.idle":"2026-09-09T17:09:56.146536Z","shell.execute_reply.started":"2026-09-09T17:09:56.130148Z","shell.execute_reply":"2026-09-09T17:09:56.145779Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nfolds = list(kf.split(X_single))\n\nfor fold, (train_idx, val_idx) in enumerate(folds, 1):\n    print(\n        f\"Fold {fold}: \"\n        f\"Train = {len(train_idx)}, \"\n        f\"Validation = {len(val_idx)}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:10:06.549478Z","iopub.execute_input":"2026-09-09T17:10:06.549842Z","iopub.status.idle":"2026-09-09T17:10:06.556922Z","shell.execute_reply.started":"2026-09-09T17:10:06.549815Z","shell.execute_reply":"2026-09-09T17:10:06.556103Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\n\ndef build_classifier(input_dim):\n\n    inputs = tf.keras.Input(\n        shape=(input_dim,)\n    )\n\n    x = tf.keras.layers.Dense(\n        128,\n        activation='relu',\n        kernel_regularizer=tf.keras.regularizers.l2(1e-4)\n    )(inputs)\n\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Dropout(0.4)(x)\n\n    x = tf.keras.layers.Dense(\n        64,\n        activation='relu',\n        kernel_regularizer=tf.keras.regularizers.l2(1e-4)\n    )(x)\n\n    x = tf.keras.layers.Dropout(0.3)(x)\n\n    outputs = tf.keras.layers.Dense(\n        12,\n        activation='sigmoid'\n    )(x)\n\n    model = tf.keras.Model(\n        inputs,\n        outputs\n    )\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(\n            learning_rate=3e-4\n        ),\n        loss='binary_crossentropy'\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:10:26.72661Z","iopub.execute_input":"2026-09-09T17:10:26.726938Z","iopub.status.idle":"2026-09-09T17:10:26.732758Z","shell.execute_reply.started":"2026-09-09T17:10:26.726913Z","shell.execute_reply":"2026-09-09T17:10:26.732059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_auc_scores(y_true, y_pred):\n\n    scores = {}\n\n    for i, target in enumerate(target_columns):\n\n        try:\n            # AUC requires both classes\n            if len(np.unique(y_true[:, i])) < 2:\n                scores[target] = np.nan\n            else:\n                scores[target] = roc_auc_score(\n                    y_true[:, i],\n                    y_pred[:, i]\n                )\n\n        except Exception:\n            scores[target] = np.nan\n\n    valid_scores = [\n        v for v in scores.values()\n        if not np.isnan(v)\n    ]\n\n    macro_auc = (\n        np.mean(valid_scores)\n        if len(valid_scores) > 0\n        else np.nan\n    )\n\n    return scores, macro_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:10:46.514786Z","iopub.execute_input":"2026-09-09T17:10:46.515178Z","iopub.status.idle":"2026-09-09T17:10:46.522608Z","shell.execute_reply.started":"2026-09-09T17:10:46.515146Z","shell.execute_reply":"2026-09-09T17:10:46.521469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"single_fold_results = []\nsingle_predictions = []\n\nfor fold, (train_idx, val_idx) in enumerate(folds, 1):\n\n    print(\"\\n\" + \"=\" * 70)\n    print(f\"SINGLE-SERIES — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    X_train = X_single[train_idx]\n    X_val = X_single[val_idx]\n\n    y_train = y[train_idx]\n    y_val = y[val_idx]\n\n    # ---------------------------------------\n    # Scale using TRAINING data only\n    # ---------------------------------------\n\n    scaler = StandardScaler()\n\n    X_train_scaled = scaler.fit_transform(\n        X_train\n    )\n\n    X_val_scaled = scaler.transform(\n        X_val\n    )\n\n    # ---------------------------------------\n    # Build model\n    # ---------------------------------------\n\n    model = build_classifier(\n        X_train_scaled.shape[1]\n    )\n\n    # ---------------------------------------\n    # Callbacks\n    # ---------------------------------------\n\n    early_stop = tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=8,\n        restore_best_weights=True\n    )\n\n    reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-6\n    )\n\n    # ---------------------------------------\n    # Train\n    # ---------------------------------------\n\n    history = model.fit(\n        X_train_scaled,\n        y_train,\n        validation_data=(\n            X_val_scaled,\n            y_val\n        ),\n        epochs=60,\n        batch_size=8,\n        callbacks=[\n            early_stop,\n            reduce_lr\n        ],\n        verbose=0\n    )\n\n    # ---------------------------------------\n    # Prediction\n    # ---------------------------------------\n\n    predictions = model.predict(\n        X_val_scaled,\n        verbose=0\n    )\n\n    # ---------------------------------------\n    # AUC\n    # ---------------------------------------\n\n    fold_scores, fold_macro = calculate_auc_scores(\n        y_val,\n        predictions\n    )\n\n    print(\"\\nFold AUC:\")\n\n    for target, score in fold_scores.items():\n\n        if np.isnan(score):\n            print(f\"{target:20s}: NaN\")\n        else:\n            print(f\"{target:20s}: {score:.4f}\")\n\n    print(\n        f\"\\nFold Macro-AUC: {fold_macro:.4f}\"\n    )\n\n    single_fold_results.append({\n        \"Fold\": fold,\n        \"Macro-AUC\": fold_macro\n    })\n\n    single_predictions.append({\n        \"fold\": fold,\n        \"val_idx\": val_idx,\n        \"y_true\": y_val,\n        \"y_pred\": predictions\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:10:50.516735Z","iopub.execute_input":"2026-09-09T17:10:50.51713Z","iopub.status.idle":"2026-09-09T17:11:03.087718Z","shell.execute_reply.started":"2026-09-09T17:10:50.5171Z","shell.execute_reply":"2026-09-09T17:11:03.086816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"single_results_df = pd.DataFrame(\n    single_fold_results\n)\n\ndisplay(single_results_df)\n\nprint(\n    \"\\nSingle-Series Mean Macro-AUC:\",\n    single_results_df[\"Macro-AUC\"].mean()\n)\n\nprint(\n    \"Single-Series Std:\",\n    single_results_df[\"Macro-AUC\"].std()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:11:09.852612Z","iopub.execute_input":"2026-09-09T17:11:09.852965Z","iopub.status.idle":"2026-09-09T17:11:09.864762Z","shell.execute_reply.started":"2026-09-09T17:11:09.852937Z","shell.execute_reply":"2026-09-09T17:11:09.863777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multi_fold_results = []\nmulti_predictions = []\n\nfor fold, (train_idx, val_idx) in enumerate(folds, 1):\n\n    print(\"\\n\" + \"=\" * 70)\n    print(f\"MULTI-SERIES — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    X_train = X_multi[train_idx]\n    X_val = X_multi[val_idx]\n\n    y_train = y[train_idx]\n    y_val = y[val_idx]\n\n    # ---------------------------------------\n    # Scale\n    # ---------------------------------------\n\n    scaler = StandardScaler()\n\n    X_train_scaled = scaler.fit_transform(\n        X_train\n    )\n\n    X_val_scaled = scaler.transform(\n        X_val\n    )\n\n    # ---------------------------------------\n    # Model\n    # ---------------------------------------\n\n    model = build_classifier(\n        X_train_scaled.shape[1]\n    )\n\n    early_stop = tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=8,\n        restore_best_weights=True\n    )\n\n    reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-6\n    )\n\n    # ---------------------------------------\n    # Train\n    # ---------------------------------------\n\n    history = model.fit(\n        X_train_scaled,\n        y_train,\n        validation_data=(\n            X_val_scaled,\n            y_val\n        ),\n        epochs=60,\n        batch_size=8,\n        callbacks=[\n            early_stop,\n            reduce_lr\n        ],\n        verbose=0\n    )\n\n    # ---------------------------------------\n    # Predict\n    # ---------------------------------------\n\n    predictions = model.predict(\n        X_val_scaled,\n        verbose=0\n    )\n\n    # ---------------------------------------\n    # AUC\n    # ---------------------------------------\n\n    fold_scores, fold_macro = calculate_auc_scores(\n        y_val,\n        predictions\n    )\n\n    print(\"\\nFold AUC:\")\n\n    for target, score in fold_scores.items():\n\n        if np.isnan(score):\n            print(f\"{target:20s}: NaN\")\n        else:\n            print(f\"{target:20s}: {score:.4f}\")\n\n    print(\n        f\"\\nFold Macro-AUC: {fold_macro:.4f}\"\n    )\n\n    multi_fold_results.append({\n        \"Fold\": fold,\n        \"Macro-AUC\": fold_macro\n    })\n\n    multi_predictions.append({\n        \"fold\": fold,\n        \"val_idx\": val_idx,\n        \"y_true\": y_val,\n        \"y_pred\": predictions\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:11:26.391553Z","iopub.execute_input":"2026-09-09T17:11:26.392029Z","iopub.status.idle":"2026-09-09T17:12:02.992375Z","shell.execute_reply.started":"2026-09-09T17:11:26.391963Z","shell.execute_reply":"2026-09-09T17:12:02.99105Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multi_results_df = pd.DataFrame(\n    multi_fold_results\n)\n\ndisplay(multi_results_df)\n\nprint(\n    \"\\nMulti-Series Mean Macro-AUC:\",\n    multi_results_df[\"Macro-AUC\"].mean()\n)\n\nprint(\n    \"Multi-Series Std:\",\n    multi_results_df[\"Macro-AUC\"].std()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:12:07.099019Z","iopub.execute_input":"2026-09-09T17:12:07.099393Z","iopub.status.idle":"2026-09-09T17:12:07.114484Z","shell.execute_reply.started":"2026-09-09T17:12:07.099356Z","shell.execute_reply":"2026-09-09T17:12:07.113212Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"comparison_results = pd.DataFrame({\n    \"Model\": [\n        \"Single-Series MRI\",\n        \"Multi-Series MRI\"\n    ],\n\n    \"Mean Macro-AUC\": [\n        single_results_df[\"Macro-AUC\"].mean(),\n        multi_results_df[\"Macro-AUC\"].mean()\n    ],\n\n    \"Std\": [\n        single_results_df[\"Macro-AUC\"].std(),\n        multi_results_df[\"Macro-AUC\"].std()\n    ]\n})\n\ndisplay(comparison_results)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:12:20.160229Z","iopub.execute_input":"2026-09-09T17:12:20.160609Z","iopub.status.idle":"2026-09-09T17:12:20.17275Z","shell.execute_reply.started":"2026-09-09T17:12:20.16058Z","shell.execute_reply":"2026-09-09T17:12:20.171401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Check missing values\n\nprint(\"Single-series feature matrix:\")\nprint(\"Shape:\", X_single.shape)\n\nprint(\n    \"Total NaN values:\",\n    np.isnan(X_single).sum()\n)\n\nprint(\n    \"Studies containing NaN:\",\n    np.isnan(X_single).any(axis=1).sum()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:14:23.012763Z","iopub.execute_input":"2026-09-09T17:14:23.013173Z","iopub.status.idle":"2026-09-09T17:14:23.020589Z","shell.execute_reply.started":"2026-09-09T17:14:23.013137Z","shell.execute_reply":"2026-09-09T17:14:23.019119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"missing_single = labeled_data.loc[\n    np.isnan(X_single).any(axis=1),\n    \"StudyInstanceUID\"\n].tolist()\n\nprint(\n    \"Studies without usable single-series features:\",\n    len(missing_single)\n)\n\nfor study in missing_single:\n    print(study)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:14:37.036149Z","iopub.execute_input":"2026-09-09T17:14:37.036493Z","iopub.status.idle":"2026-09-09T17:14:37.043145Z","shell.execute_reply.started":"2026-09-09T17:14:37.036465Z","shell.execute_reply":"2026-09-09T17:14:37.042098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"valid_single_mask = ~np.isnan(X_single).any(axis=1)\n\nX_single_valid = X_single[\n    valid_single_mask\n]\n\ny_single_valid = y[\n    valid_single_mask\n]\n\nsingle_ids_valid = labeled_data.loc[\n    valid_single_mask,\n    \"StudyInstanceUID\"\n].values\n\nprint(\"Original studies:\", len(labeled_data))\nprint(\"Usable single-series studies:\", len(X_single_valid))\nprint(\"Removed studies:\", (~valid_single_mask).sum())\n\nprint(\"X shape:\", X_single_valid.shape)\nprint(\"Y shape:\", y_single_valid.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:14:52.164407Z","iopub.execute_input":"2026-09-09T17:14:52.164742Z","iopub.status.idle":"2026-09-09T17:14:52.173498Z","shell.execute_reply.started":"2026-09-09T17:14:52.164705Z","shell.execute_reply":"2026-09-09T17:14:52.172167Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kf_single = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nsingle_folds = list(\n    kf_single.split(X_single_valid)\n)\n\nfor fold, (train_idx, val_idx) in enumerate(\n    single_folds, 1\n):\n    print(\n        f\"Fold {fold}: \"\n        f\"Train={len(train_idx)}, \"\n        f\"Validation={len(val_idx)}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:15:03.002145Z","iopub.execute_input":"2026-09-09T17:15:03.002493Z","iopub.status.idle":"2026-09-09T17:15:03.010227Z","shell.execute_reply.started":"2026-09-09T17:15:03.002465Z","shell.execute_reply":"2026-09-09T17:15:03.009174Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"single_fold_results = []\nsingle_predictions = []\n\nfor fold, (train_idx, val_idx) in enumerate(\n    single_folds, 1\n):\n\n    print(\"\\n\" + \"=\" * 70)\n    print(f\"SINGLE-SERIES — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    X_train = X_single_valid[train_idx]\n    X_val = X_single_valid[val_idx]\n\n    y_train = y_single_valid[train_idx]\n    y_val = y_single_valid[val_idx]\n\n    # -----------------------------------\n    # Scaling\n    # -----------------------------------\n\n    scaler = StandardScaler()\n\n    X_train_scaled = scaler.fit_transform(\n        X_train\n    )\n\n    X_val_scaled = scaler.transform(\n        X_val\n    )\n\n    # -----------------------------------\n    # Safety check\n    # -----------------------------------\n\n    assert not np.isnan(\n        X_train_scaled\n    ).any()\n\n    assert not np.isnan(\n        X_val_scaled\n    ).any()\n\n    # -----------------------------------\n    # Model\n    # -----------------------------------\n\n    model = build_classifier(\n        X_train_scaled.shape[1]\n    )\n\n    early_stop = tf.keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=8,\n        restore_best_weights=True\n    )\n\n    reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=3,\n        min_lr=1e-6\n    )\n\n    # -----------------------------------\n    # Training\n    # -----------------------------------\n\n    history = model.fit(\n        X_train_scaled,\n        y_train,\n        validation_data=(\n            X_val_scaled,\n            y_val\n        ),\n        epochs=60,\n        batch_size=8,\n        callbacks=[\n            early_stop,\n            reduce_lr\n        ],\n        verbose=0\n    )\n\n    # -----------------------------------\n    # Prediction\n    # -----------------------------------\n\n    predictions = model.predict(\n        X_val_scaled,\n        verbose=0\n    )\n\n    # -----------------------------------\n    # AUC\n    # -----------------------------------\n\n    fold_scores, fold_macro = calculate_auc_scores(\n        y_val,\n        predictions\n    )\n\n    print(\"\\nFold AUC:\")\n\n    for target, score in fold_scores.items():\n\n        if np.isnan(score):\n            print(f\"{target:20s}: NaN\")\n        else:\n            print(\n                f\"{target:20s}: {score:.4f}\"\n            )\n\n    print(\n        f\"\\nFold Macro-AUC: {fold_macro:.4f}\"\n    )\n\n    single_fold_results.append({\n        \"Fold\": fold,\n        \"Macro-AUC\": fold_macro\n    })\n\n    single_predictions.append({\n        \"fold\": fold,\n        \"val_idx\": val_idx,\n        \"y_true\": y_val,\n        \"y_pred\": predictions\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:15:12.674715Z","iopub.execute_input":"2026-09-09T17:15:12.675084Z","iopub.status.idle":"2026-09-09T17:15:41.264352Z","shell.execute_reply.started":"2026-09-09T17:15:12.675055Z","shell.execute_reply":"2026-09-09T17:15:41.262675Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"single_results_df = pd.DataFrame(\n    single_fold_results\n)\n\ndisplay(single_results_df)\n\nprint(\n    \"\\nSingle-Series Mean Macro-AUC:\",\n    single_results_df[\"Macro-AUC\"].mean()\n)\n\nprint(\n    \"Single-Series Std:\",\n    single_results_df[\"Macro-AUC\"].std()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:15:45.272826Z","iopub.execute_input":"2026-09-09T17:15:45.273225Z","iopub.status.idle":"2026-09-09T17:15:45.284851Z","shell.execute_reply.started":"2026-09-09T17:15:45.273191Z","shell.execute_reply":"2026-09-09T17:15:45.283971Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\n\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nprint(\"TensorFlow:\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:21:09.450607Z","iopub.execute_input":"2026-09-09T17:21:09.45107Z","iopub.status.idle":"2026-09-09T17:21:09.458127Z","shell.execute_reply.started":"2026-09-09T17:21:09.451033Z","shell.execute_reply":"2026-09-09T17:21:09.456914Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_PATH = \"/kaggle/input/datasets/alenic/rsna-knee-abnormality-detection-jpeg-224x224\"\n\nTRAIN_SERIES_PATH = os.path.join(\n    DATA_PATH,\n    \"train_series\"\n)\n\nprint(os.listdir(DATA_PATH))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:21:22.83849Z","iopub.execute_input":"2026-09-09T17:21:22.838863Z","iopub.status.idle":"2026-09-09T17:21:22.86388Z","shell.execute_reply.started":"2026-09-09T17:21:22.838831Z","shell.execute_reply":"2026-09-09T17:21:22.862637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv(\n    os.path.join(DATA_PATH, \"train.csv\")\n)\n\ntrain_series = pd.read_csv(\n    os.path.join(DATA_PATH, \"train_series.csv\")\n)\n\ntest = pd.read_csv(\n    os.path.join(DATA_PATH, \"test.csv\")\n)\n\ntest_series = pd.read_csv(\n    os.path.join(DATA_PATH, \"test_series.csv\")\n)\n\nsample_submission = pd.read_csv(\n    os.path.join(DATA_PATH, \"sample_submission.csv\")\n)\n\nprint(\"Train:\", train.shape)\nprint(\"Train Series:\", train_series.shape)\nprint(\"Test:\", test.shape)\nprint(\"Test Series:\", test_series.shape)\nprint(\"Submission:\", sample_submission.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:21:37.112829Z","iopub.execute_input":"2026-09-09T17:21:37.113205Z","iopub.status.idle":"2026-09-09T17:21:37.253851Z","shell.execute_reply.started":"2026-09-09T17:21:37.113167Z","shell.execute_reply":"2026-09-09T17:21:37.252687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_columns = [\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 targets:\", len(target_columns))\nprint(target_columns)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:21:52.7766Z","iopub.execute_input":"2026-09-09T17:21:52.776951Z","iopub.status.idle":"2026-09-09T17:21:52.78362Z","shell.execute_reply.started":"2026-09-09T17:21:52.776921Z","shell.execute_reply":"2026-09-09T17:21:52.782362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_mask = train[target_columns].notna().all(axis=1)\n\nlabeled_data = train[\n    labeled_mask\n].copy()\n\nlabeled_studies = labeled_data[\n    \"StudyInstanceUID\"\n].tolist()\n\nprint(\n    \"Officially labeled studies:\",\n    len(labeled_studies)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:22:05.443491Z","iopub.execute_input":"2026-09-09T17:22:05.443886Z","iopub.status.idle":"2026-09-09T17:22:05.45403Z","shell.execute_reply.started":"2026-09-09T17:22:05.44385Z","shell.execute_reply":"2026-09-09T17:22:05.453013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_series = train_series[\n    train_series[\"StudyInstanceUID\"].isin(\n        labeled_studies\n    )\n].copy()\n\nprint(\n    \"Labeled-study series:\",\n    labeled_series.shape\n)\n\ndisplay(\n    labeled_series.head()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:22:15.361576Z","iopub.execute_input":"2026-09-09T17:22:15.361952Z","iopub.status.idle":"2026-09-09T17:22:15.374571Z","shell.execute_reply.started":"2026-09-09T17:22:15.36192Z","shell.execute_reply":"2026-09-09T17:22:15.373771Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"candidate_series = labeled_series[\n    (labeled_series[\"Fluid_Sensitive\"] == 1) &\n    (labeled_series[\"Fat_Suppression\"] == 1)\n].copy()\n\nprint(\n    \"Candidate series:\",\n    len(candidate_series)\n)\n\nprint(\n    candidate_series[\"Anatomical_Plane\"]\n    .value_counts()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:22:27.589544Z","iopub.execute_input":"2026-09-09T17:22:27.589894Z","iopub.status.idle":"2026-09-09T17:22:27.598673Z","shell.execute_reply.started":"2026-09-09T17:22:27.589867Z","shell.execute_reply":"2026-09-09T17:22:27.59756Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_image_files(study_id, series_id):\n\n    folder = os.path.join(\n        TRAIN_SERIES_PATH,\n        str(study_id),\n        str(series_id)\n    )\n\n    if not os.path.exists(folder):\n        return []\n\n    files = [\n        os.path.join(folder, f)\n        for f in os.listdir(folder)\n        if f.lower().endswith(\n            (\".jpg\", \".jpeg\", \".png\")\n        )\n    ]\n\n    files.sort()\n\n    return files","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:23:01.241464Z","iopub.execute_input":"2026-09-09T17:23:01.241857Z","iopub.status.idle":"2026-09-09T17:23:01.250301Z","shell.execute_reply.started":"2026-09-09T17:23:01.241822Z","shell.execute_reply":"2026-09-09T17:23:01.249267Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_series_rows = []\n\nfor study_id in labeled_studies:\n\n    study_rows = candidate_series[\n        candidate_series[\"StudyInstanceUID\"]\n        == study_id\n    ]\n\n    for plane in [\n        \"Sagittal\",\n        \"Coronal\",\n        \"Axial\"\n    ]:\n\n        plane_rows = study_rows[\n            study_rows[\"Anatomical_Plane\"]\n            == plane\n        ].copy()\n\n        if len(plane_rows) == 0:\n            continue\n\n        plane_rows[\"num_images\"] = plane_rows.apply(\n            lambda row: len(\n                get_image_files(\n                    row[\"StudyInstanceUID\"],\n                    row[\"SeriesInstanceUID\"]\n                )\n            ),\n            axis=1\n        )\n\n        # Require at least 8 slices\n        plane_rows = plane_rows[\n            plane_rows[\"num_images\"] >= 8\n        ]\n\n        if len(plane_rows) == 0:\n            continue\n\n        # Choose the series with most slices\n        best_row = plane_rows.loc[\n            plane_rows[\"num_images\"].idxmax()\n        ]\n\n        best_series_rows.append(\n            best_row\n        )\n\nbest_series = pd.DataFrame(\n    best_series_rows\n).reset_index(drop=True)\n\nprint(\n    \"Selected series:\",\n    best_series.shape\n)\n\ndisplay(\n    best_series.head(10)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:23:13.432254Z","iopub.execute_input":"2026-09-09T17:23:13.432597Z","iopub.status.idle":"2026-09-09T17:23:14.121584Z","shell.execute_reply.started":"2026-09-09T17:23:13.432564Z","shell.execute_reply":"2026-09-09T17:23:14.120485Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 224\n\ndef load_image(path):\n\n    image = Image.open(path).convert(\"L\")\n\n    image = image.resize(\n        (IMG_SIZE, IMG_SIZE)\n    )\n\n    image = np.array(\n        image,\n        dtype=np.float32\n    )\n\n    # grayscale → RGB\n    image = np.expand_dims(\n        image,\n        axis=-1\n    )\n\n    image = np.repeat(\n        image,\n        3,\n        axis=-1\n    )\n\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:23:55.291024Z","iopub.execute_input":"2026-09-09T17:23:55.291398Z","iopub.status.idle":"2026-09-09T17:23:55.298317Z","shell.execute_reply.started":"2026-09-09T17:23:55.291362Z","shell.execute_reply":"2026-09-09T17:23:55.296981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_model = tf.keras.applications.MobileNetV2(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(224, 224, 3)\n)\n\nbase_model.trainable = False\n\nfeature_extractor = tf.keras.Model(\n    inputs=base_model.input,\n    outputs=base_model.output\n)\n\nprint(\n    \"Feature output:\",\n    feature_extractor.output_shape\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:24:10.793181Z","iopub.execute_input":"2026-09-09T17:24:10.793503Z","iopub.status.idle":"2026-09-09T17:24:11.357137Z","shell.execute_reply.started":"2026-09-09T17:24:10.793476Z","shell.execute_reply":"2026-09-09T17:24:11.355773Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_SLICES = 8\n\ndef select_8_slices(image_files):\n\n    if len(image_files) <= NUM_SLICES:\n        return image_files\n\n    indices = np.linspace(\n        0,\n        len(image_files) - 1,\n        NUM_SLICES\n    ).astype(int)\n\n    return [\n        image_files[i]\n        for i in indices\n    ]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:24:45.512638Z","iopub.execute_input":"2026-09-09T17:24:45.512957Z","iopub.status.idle":"2026-09-09T17:24:45.518649Z","shell.execute_reply.started":"2026-09-09T17:24:45.512929Z","shell.execute_reply":"2026-09-09T17:24:45.517627Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_series_feature_8(\n    study_id,\n    series_id\n):\n\n    image_files = get_image_files(\n        study_id,\n        series_id\n    )\n\n    if len(image_files) < 8:\n        return None\n\n    selected_files = select_8_slices(\n        image_files\n    )\n\n    images = np.array(\n        [\n            load_image(path)\n            for path in selected_files\n        ],\n        dtype=np.float32\n    )\n\n    images = (\n        tf.keras.applications\n        .mobilenet_v2\n        .preprocess_input(images)\n    )\n\n    features = feature_extractor.predict(\n        images,\n        verbose=0\n    )\n\n    # Shape:\n    # (8, 7, 7, 1280)\n\n    # Average across:\n    # slices + height + width\n\n    feature = features.mean(\n        axis=(0, 1, 2)\n    )\n\n    return feature","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:26:32.630621Z","iopub.execute_input":"2026-09-09T17:26:32.631014Z","iopub.status.idle":"2026-09-09T17:26:32.638376Z","shell.execute_reply.started":"2026-09-09T17:26:32.630952Z","shell.execute_reply":"2026-09-09T17:26:32.637124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multi8_features = []\n\nstudy_list = best_series[\n    \"StudyInstanceUID\"\n].unique()\n\nfor study_id in tqdm(\n    study_list,\n    desc=\"Extracting 8-slice features\"\n):\n\n    result = {\n        \"StudyInstanceUID\": study_id\n    }\n\n    study_rows = best_series[\n        best_series[\"StudyInstanceUID\"]\n        == study_id\n    ]\n\n    for plane in [\n        \"Sagittal\",\n        \"Coronal\",\n        \"Axial\"\n    ]:\n\n        rows = study_rows[\n            study_rows[\"Anatomical_Plane\"]\n            == plane\n        ]\n\n        if len(rows) == 0:\n            continue\n\n        row = rows.iloc[0]\n\n        feature = extract_series_feature_8(\n            row[\"StudyInstanceUID\"],\n            row[\"SeriesInstanceUID\"]\n        )\n\n        if feature is None:\n            continue\n\n        for i, value in enumerate(feature):\n\n            result[\n                f\"{plane}_{i}\"\n            ] = value\n\n    multi8_features.append(result)\n\n\nmulti8_features = pd.DataFrame(\n    multi8_features\n)\n\nprint(\"\\nFinished!\")\nprint(\n    \"Feature matrix:\",\n    multi8_features.shape\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:26:44.614204Z","iopub.execute_input":"2026-09-09T17:26:44.61454Z","iopub.status.idle":"2026-09-09T17:27:24.928818Z","shell.execute_reply.started":"2026-09-09T17:26:44.614512Z","shell.execute_reply":"2026-09-09T17:27:24.928012Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_columns_8 = [\n    c\n    for c in multi8_features.columns\n    if c != \"StudyInstanceUID\"\n]\n\naligned_8 = labeled_data[\n    [\"StudyInstanceUID\"]\n].merge(\n    multi8_features,\n    on=\"StudyInstanceUID\",\n    how=\"left\"\n)\n\nX_8 = aligned_8[\n    feature_columns_8\n].values.astype(\n    np.float32\n)\n\ny_8 = labeled_data[\n    target_columns\n].values.astype(\n    np.float32\n)\n\nprint(\"X:\", X_8.shape)\nprint(\"Y:\", y_8.shape)\n\nprint(\n    \"NaN values:\",\n    np.isnan(X_8).sum()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:27:42.381241Z","iopub.execute_input":"2026-09-09T17:27:42.381609Z","iopub.status.idle":"2026-09-09T17:27:42.39716Z","shell.execute_reply.started":"2026-09-09T17:27:42.381574Z","shell.execute_reply":"2026-09-09T17:27:42.396347Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_classifier(input_dim):\n\n    inputs = tf.keras.Input(\n        shape=(input_dim,)\n    )\n\n    x = tf.keras.layers.Dense(\n        128,\n        activation=\"relu\",\n        kernel_regularizer=\n        tf.keras.regularizers.l2(1e-4)\n    )(inputs)\n\n    x = tf.keras.layers.BatchNormalization()(x)\n\n    x = tf.keras.layers.Dropout(\n        0.4\n    )(x)\n\n    x = tf.keras.layers.Dense(\n        64,\n        activation=\"relu\",\n        kernel_regularizer=\n        tf.keras.regularizers.l2(1e-4)\n    )(x)\n\n    x = tf.keras.layers.Dropout(\n        0.3\n    )(x)\n\n    outputs = tf.keras.layers.Dense(\n        12,\n        activation=\"sigmoid\"\n    )(x)\n\n    model = tf.keras.Model(\n        inputs,\n        outputs\n    )\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(\n            learning_rate=3e-4\n        ),\n        loss=\"binary_crossentropy\"\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:27:58.513247Z","iopub.execute_input":"2026-09-09T17:27:58.513589Z","iopub.status.idle":"2026-09-09T17:27:58.52102Z","shell.execute_reply.started":"2026-09-09T17:27:58.513561Z","shell.execute_reply":"2026-09-09T17:27:58.519925Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import roc_auc_score\n\nkf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nfold_scores_8 = []\n\nfor fold, (train_idx, val_idx) in enumerate(\n    kf.split(X_8),\n    start=1\n):\n\n    print(\"\\n\")\n    print(\"=\" * 70)\n    print(\n        f\"8-SLICE MULTI-SERIES — FOLD {fold}\"\n    )\n    print(\"=\" * 70)\n\n    X_train = X_8[train_idx]\n    X_val = X_8[val_idx]\n\n    y_train = y_8[train_idx]\n    y_val = y_8[val_idx]\n\n    # Scale\n    scaler = StandardScaler()\n\n    X_train_scaled = scaler.fit_transform(\n        X_train\n    )\n\n    X_val_scaled = scaler.transform(\n        X_val\n    )\n\n    # Model\n    model = build_classifier(\n        X_train_scaled.shape[1]\n    )\n\n    early_stop = tf.keras.callbacks.EarlyStopping(\n        monitor=\"val_loss\",\n        patience=5,\n        restore_best_weights=True\n    )\n\n    model.fit(\n        X_train_scaled,\n        y_train,\n        validation_data=(\n            X_val_scaled,\n            y_val\n        ),\n        epochs=40,\n        batch_size=8,\n        callbacks=[early_stop],\n        verbose=0\n    )\n\n    # Predict\n    predictions = model.predict(\n        X_val_scaled,\n        verbose=0\n    )\n\n    aucs = []\n\n    for i, target in enumerate(\n        target_columns\n    ):\n\n        try:\n\n            auc = roc_auc_score(\n                y_val[:, i],\n                predictions[:, i]\n            )\n\n            aucs.append(auc)\n\n            print(\n                f\"{target:20s}: {auc:.4f}\"\n            )\n\n        except ValueError:\n\n            aucs.append(np.nan)\n\n            print(\n                f\"{target:20s}: NaN\"\n            )\n\n    macro_auc = np.nanmean(aucs)\n\n    print(\n        f\"\\nFold Macro-AUC: \"\n        f\"{macro_auc:.4f}\"\n    )\n\n    fold_scores_8.append(\n        macro_auc\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:28:09.980771Z","iopub.execute_input":"2026-09-09T17:28:09.981174Z","iopub.status.idle":"2026-09-09T17:28:21.958915Z","shell.execute_reply.started":"2026-09-09T17:28:09.981142Z","shell.execute_reply":"2026-09-09T17:28:21.958062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mean_8 = np.nanmean(\n    fold_scores_8\n)\n\nstd_8 = np.nanstd(\n    fold_scores_8\n)\n\nprint(\"\\n\")\nprint(\"=\" * 70)\nprint(\"STEP 16 — FINAL RESULT\")\nprint(\"=\" * 70)\n\nprint(\n    \"Fold scores:\",\n    fold_scores_8\n)\n\nprint(\n    f\"Mean Macro-AUC: {mean_8:.6f}\"\n)\n\nprint(\n    f\"Std: {std_8:.6f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:28:29.616514Z","iopub.execute_input":"2026-09-09T17:28:29.617605Z","iopub.status.idle":"2026-09-09T17:28:29.629188Z","shell.execute_reply.started":"2026-09-09T17:28:29.617567Z","shell.execute_reply":"2026-09-09T17:28:29.628104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK WHICH PLANES ARE MISSING\n# ============================================================\n\nplane_feature_counts = {}\n\nfor plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n    cols = [\n        c for c in multi8_features.columns\n        if c.startswith(plane + \"_\")\n    ]\n\n    plane_feature_counts[plane] = (\n        multi8_features[cols]\n        .notna()\n        .all(axis=1)\n        .sum()\n    )\n\nprint(\"Studies with complete features:\")\nfor plane, count in plane_feature_counts.items():\n    print(f\"{plane:10s}: {count}/58\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:30:36.177033Z","iopub.execute_input":"2026-09-09T17:30:36.177432Z","iopub.status.idle":"2026-09-09T17:30:36.192931Z","shell.execute_reply.started":"2026-09-09T17:30:36.177402Z","shell.execute_reply":"2026-09-09T17:30:36.192122Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# CHECK MISSING VALUES PER STUDY\n# ============================================================\n\nfeature_cols = [\n    c for c in multi8_features.columns\n    if c != \"StudyInstanceUID\"\n]\n\nmissing_per_study = (\n    multi8_features[feature_cols]\n    .isna()\n    .sum(axis=1)\n)\n\nprint(\"Missing feature values per study:\")\ndisplay(\n    pd.DataFrame({\n        \"StudyInstanceUID\":\n            multi8_features[\"StudyInstanceUID\"],\n        \"Missing_Features\":\n            missing_per_study\n    }).sort_values(\n        \"Missing_Features\",\n        ascending=False\n    ).head(20)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:30:47.536123Z","iopub.execute_input":"2026-09-09T17:30:47.537547Z","iopub.status.idle":"2026-09-09T17:30:47.553858Z","shell.execute_reply.started":"2026-09-09T17:30:47.53749Z","shell.execute_reply":"2026-09-09T17:30:47.553154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# FILL MISSING PLANE FEATURES\n# ============================================================\n\nmulti8_fixed = multi8_features.copy()\n\nfeature_columns = [\n    c for c in multi8_fixed.columns\n    if c != \"StudyInstanceUID\"\n]\n\nmulti8_fixed[feature_columns] = (\n    multi8_fixed[feature_columns]\n    .fillna(0.0)\n)\n\nprint(\n    \"Total NaN after filling:\",\n    multi8_fixed[feature_columns]\n    .isna()\n    .sum()\n    .sum()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:31:02.056944Z","iopub.execute_input":"2026-09-09T17:31:02.057352Z","iopub.status.idle":"2026-09-09T17:31:02.671193Z","shell.execute_reply.started":"2026-09-09T17:31:02.057321Z","shell.execute_reply":"2026-09-09T17:31:02.670291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# ALIGN FEATURES WITH LABELS\n# ============================================================\n\naligned_8 = labeled_data[\n    [\"StudyInstanceUID\"]\n].merge(\n    multi8_fixed,\n    on=\"StudyInstanceUID\",\n    how=\"left\"\n)\n\nfeature_columns_8 = [\n    c for c in aligned_8.columns\n    if c != \"StudyInstanceUID\"\n]\n\nX_8 = aligned_8[\n    feature_columns_8\n].values.astype(np.float32)\n\ny_8 = labeled_data[\n    target_columns\n].values.astype(np.float32)\n\nprint(\"X shape:\", X_8.shape)\nprint(\"Y shape:\", y_8.shape)\n\nprint(\n    \"NaN:\",\n    np.isnan(X_8).sum()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:31:14.390357Z","iopub.execute_input":"2026-09-09T17:31:14.390671Z","iopub.status.idle":"2026-09-09T17:31:15.423929Z","shell.execute_reply.started":"2026-09-09T17:31:14.390647Z","shell.execute_reply":"2026-09-09T17:31:15.42307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import KFold\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import roc_auc_score\n\nkf = KFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nfold_scores_8 = []\n\nfor fold, (train_idx, val_idx) in enumerate(\n    kf.split(X_8),\n    start=1\n):\n\n    print(\"\\n\")\n    print(\"=\" * 70)\n    print(f\"8-SLICE MULTI-SERIES — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    X_train = X_8[train_idx]\n    X_val = X_8[val_idx]\n\n    y_train = y_8[train_idx]\n    y_val = y_8[val_idx]\n\n    # Scale using TRAINING data only\n    scaler = StandardScaler()\n\n    X_train_scaled = scaler.fit_transform(\n        X_train\n    )\n\n    X_val_scaled = scaler.transform(\n        X_val\n    )\n\n    # Build model\n    model = build_classifier(\n        X_train_scaled.shape[1]\n    )\n\n    early_stop = tf.keras.callbacks.EarlyStopping(\n        monitor=\"val_loss\",\n        patience=5,\n        restore_best_weights=True\n    )\n\n    model.fit(\n        X_train_scaled,\n        y_train,\n        validation_data=(\n            X_val_scaled,\n            y_val\n        ),\n        epochs=40,\n        batch_size=8,\n        callbacks=[early_stop],\n        verbose=0\n    )\n\n    predictions = model.predict(\n        X_val_scaled,\n        verbose=0\n    )\n\n    aucs = []\n\n    for i, target in enumerate(\n        target_columns\n    ):\n\n        try:\n\n            auc = roc_auc_score(\n                y_val[:, i],\n                predictions[:, i]\n            )\n\n            aucs.append(auc)\n\n            print(\n                f\"{target:20s}: {auc:.4f}\"\n            )\n\n        except ValueError:\n\n            aucs.append(np.nan)\n\n            print(\n                f\"{target:20s}: NaN\"\n            )\n\n    macro_auc = np.nanmean(aucs)\n\n    print(\n        f\"\\nFold Macro-AUC: \"\n        f\"{macro_auc:.4f}\"\n    )\n\n    fold_scores_8.append(\n        macro_auc\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:31:30.816146Z","iopub.execute_input":"2026-09-09T17:31:30.81652Z","iopub.status.idle":"2026-09-09T17:31:58.740201Z","shell.execute_reply.started":"2026-09-09T17:31:30.816487Z","shell.execute_reply":"2026-09-09T17:31:58.739155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n\")\nprint(\"=\" * 70)\nprint(\"STEP 16 — 8-SLICE FINAL RESULT\")\nprint(\"=\" * 70)\n\nprint(\n    \"Fold scores:\",\n    fold_scores_8\n)\n\nprint(\n    f\"Mean Macro-AUC: \"\n    f\"{np.nanmean(fold_scores_8):.6f}\"\n)\n\nprint(\n    f\"Std: \"\n    f\"{np.nanstd(fold_scores_8):.6f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:32:12.962471Z","iopub.execute_input":"2026-09-09T17:32:12.96284Z","iopub.status.idle":"2026-09-09T17:32:12.969639Z","shell.execute_reply.started":"2026-09-09T17:32:12.96281Z","shell.execute_reply":"2026-09-09T17:32:12.96839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\n\nfrom tqdm.auto import tqdm\n\nprint(\"TensorFlow:\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:34:53.677866Z","iopub.execute_input":"2026-09-09T17:34:53.678213Z","iopub.status.idle":"2026-09-09T17:34:53.684828Z","shell.execute_reply.started":"2026-09-09T17:34:53.678186Z","shell.execute_reply":"2026-09-09T17:34:53.68407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_PATH = \"/kaggle/input/datasets/alenic/rsna-knee-abnormality-detection-jpeg-224x224\"\n\ntrain = pd.read_csv(os.path.join(DATA_PATH, \"train.csv\"))\ntrain_series = pd.read_csv(os.path.join(DATA_PATH, \"train_series.csv\"))\n\ntarget_columns = [\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\nlabeled_data = train.dropna(subset=target_columns).copy()\n\nprint(\"Labeled studies:\", len(labeled_data))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:35:04.836797Z","iopub.execute_input":"2026-09-09T17:35:04.837147Z","iopub.status.idle":"2026-09-09T17:35:04.961835Z","shell.execute_reply.started":"2026-09-09T17:35:04.837118Z","shell.execute_reply":"2026-09-09T17:35:04.960674Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labeled_uids = set(labeled_data[\"StudyInstanceUID\"])\n\nlabeled_series = train_series[\n    train_series[\"StudyInstanceUID\"].isin(labeled_uids)\n].copy()\n\npreferred_series = labeled_series[\n    (labeled_series[\"Fluid_Sensitive\"] == 1) &\n    (labeled_series[\"Fat_Suppression\"] == 1)\n].copy()\n\nprint(\"Preferred series:\", len(preferred_series))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:35:17.65406Z","iopub.execute_input":"2026-09-09T17:35:17.654394Z","iopub.status.idle":"2026-09-09T17:35:17.664862Z","shell.execute_reply.started":"2026-09-09T17:35:17.654364Z","shell.execute_reply":"2026-09-09T17:35:17.663641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_image_files(study_uid, series_uid):\n    folder = os.path.join(\n        DATA_PATH,\n        \"train_series\",\n        study_uid,\n        series_uid\n    )\n    \n    if not os.path.exists(folder):\n        return []\n    \n    files = [\n        os.path.join(folder, f)\n        for f in os.listdir(folder)\n        if f.lower().endswith((\".jpg\", \".jpeg\", \".png\"))\n    ]\n    \n    files.sort()\n    return files","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:35:32.576638Z","iopub.execute_input":"2026-09-09T17:35:32.577059Z","iopub.status.idle":"2026-09-09T17:35:32.583776Z","shell.execute_reply.started":"2026-09-09T17:35:32.577028Z","shell.execute_reply":"2026-09-09T17:35:32.582629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"selected_series = {}\n\nfor study_uid in labeled_uids:\n    \n    study_rows = preferred_series[\n        preferred_series[\"StudyInstanceUID\"] == study_uid\n    ]\n    \n    selected_series[study_uid] = {}\n    \n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n        \n        plane_rows = study_rows[\n            study_rows[\"Anatomical_Plane\"] == plane\n        ]\n        \n        best_series = None\n        best_count = 0\n        \n        for _, row in plane_rows.iterrows():\n            \n            files = get_image_files(\n                study_uid,\n                row[\"SeriesInstanceUID\"]\n            )\n            \n            if len(files) >= 8 and len(files) > best_count:\n                best_series = row[\"SeriesInstanceUID\"]\n                best_count = len(files)\n        \n        if best_series is not None:\n            selected_series[study_uid][plane] = best_series\n\nprint(\"Series selection complete.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:35:45.879824Z","iopub.execute_input":"2026-09-09T17:35:45.88018Z","iopub.status.idle":"2026-09-09T17:35:46.416132Z","shell.execute_reply.started":"2026-09-09T17:35:45.880151Z","shell.execute_reply":"2026-09-09T17:35:46.415183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = (224, 224)\n\ndef load_image(path):\n    img = Image.open(path).convert(\"RGB\")\n    img = img.resize(IMG_SIZE)\n    img = np.array(img).astype(np.float32) / 255.0\n    return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:36:07.584015Z","iopub.execute_input":"2026-09-09T17:36:07.584445Z","iopub.status.idle":"2026-09-09T17:36:07.593305Z","shell.execute_reply.started":"2026-09-09T17:36:07.584416Z","shell.execute_reply":"2026-09-09T17:36:07.591519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_model = keras.applications.MobileNetV2(\n    input_shape=(224, 224, 3),\n    include_top=False,\n    weights=\"imagenet\"\n)\n\nbase_model.trainable = False\n\nprint(\"MobileNetV2 loaded.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:36:22.10442Z","iopub.execute_input":"2026-09-09T17:36:22.104797Z","iopub.status.idle":"2026-09-09T17:36:22.712375Z","shell.execute_reply.started":"2026-09-09T17:36:22.104769Z","shell.execute_reply":"2026-09-09T17:36:22.711535Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def select_8_slices(files):\n    indices = np.linspace(\n        0,\n        len(files) - 1,\n        8\n    ).astype(int)\n    \n    return [files[i] for i in indices]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:36:43.924425Z","iopub.execute_input":"2026-09-09T17:36:43.924817Z","iopub.status.idle":"2026-09-09T17:36:43.93127Z","shell.execute_reply.started":"2026-09-09T17:36:43.924784Z","shell.execute_reply":"2026-09-09T17:36:43.930119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_slice_features(files):\n    \n    selected = select_8_slices(files)\n    \n    images = np.stack([\n        load_image(path)\n        for path in selected\n    ])\n    \n    images = keras.applications.mobilenet_v2.preprocess_input(\n        images * 255.0\n    )\n    \n    features = base_model.predict(\n        images,\n        verbose=0\n    )\n    \n    # Global average pooling over spatial dimensions\n    features = features.mean(axis=(1, 2))\n    \n    return features.astype(np.float32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:36:57.947813Z","iopub.execute_input":"2026-09-09T17:36:57.948204Z","iopub.status.idle":"2026-09-09T17:36:57.954034Z","shell.execute_reply.started":"2026-09-09T17:36:57.948173Z","shell.execute_reply":"2026-09-09T17:36:57.953113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"multi17_features = {}\n\nfor study_uid in tqdm(\n    labeled_uids,\n    desc=\"Extracting Step 17 features\"\n):\n    \n    multi17_features[study_uid] = {}\n    \n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n        \n        if plane not in selected_series[study_uid]:\n            continue\n        \n        series_uid = selected_series[study_uid][plane]\n        \n        files = get_image_files(\n            study_uid,\n            series_uid\n        )\n        \n        if len(files) < 8:\n            continue\n        \n        features = extract_slice_features(files)\n        \n        multi17_features[study_uid][plane] = features\n\nprint(\"Feature extraction finished.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:37:10.60168Z","iopub.execute_input":"2026-09-09T17:37:10.602098Z","iopub.status.idle":"2026-09-09T17:37:47.511466Z","shell.execute_reply.started":"2026-09-09T17:37:10.602056Z","shell.execute_reply":"2026-09-09T17:37:47.510365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for study_uid in list(multi17_features.keys())[:3]:\n    \n    print(\"\\nStudy:\", study_uid)\n    \n    for plane, features in multi17_features[study_uid].items():\n        print(plane, features.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:38:08.836534Z","iopub.execute_input":"2026-09-09T17:38:08.836944Z","iopub.status.idle":"2026-09-09T17:38:08.842725Z","shell.execute_reply.started":"2026-09-09T17:38:08.836909Z","shell.execute_reply":"2026-09-09T17:38:08.841799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SliceAttention(layers.Layer):\n    \n    def __init__(self, hidden_dim=128, **kwargs):\n        super().__init__(**kwargs)\n        \n        self.fc1 = layers.Dense(\n            hidden_dim,\n            activation=\"tanh\"\n        )\n        \n        self.fc2 = layers.Dense(1)\n    \n    def call(self, x):\n        \"\"\"\n        x shape:\n        (batch, 8, 1280)\n        \"\"\"\n        \n        scores = self.fc2(\n            self.fc1(x)\n        )\n        \n        # (batch, 8, 1)\n        weights = tf.nn.softmax(\n            scores,\n            axis=1\n        )\n        \n        # Weighted sum over slices\n        weighted = x * weights\n        \n        output = tf.reduce_sum(\n            weighted,\n            axis=1\n        )\n        \n        return output","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:38:22.139265Z","iopub.execute_input":"2026-09-09T17:38:22.139637Z","iopub.status.idle":"2026-09-09T17:38:22.146703Z","shell.execute_reply.started":"2026-09-09T17:38:22.139607Z","shell.execute_reply":"2026-09-09T17:38:22.145646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X17 = []\nY17 = []\n\nfor _, row in labeled_data.iterrows():\n    \n    study_uid = row[\"StudyInstanceUID\"]\n    \n    plane_features = []\n    \n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n        \n        if plane in multi17_features[study_uid]:\n            plane_features.append(\n                multi17_features[study_uid][plane]\n            )\n        else:\n            plane_features.append(\n                np.zeros((8, 1280), dtype=np.float32)\n            )\n    \n    # Shape:\n    # (3, 8, 1280)\n    study_features = np.stack(\n        plane_features\n    )\n    \n    X17.append(study_features)\n    Y17.append(\n        row[target_columns].values.astype(np.float32)\n    )\n\nX17 = np.stack(X17)\nY17 = np.stack(Y17)\n\nprint(\"X17:\", X17.shape)\nprint(\"Y17:\", Y17.shape)\nprint(\"NaN:\", np.isnan(X17).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:38:35.436005Z","iopub.execute_input":"2026-09-09T17:38:35.436371Z","iopub.status.idle":"2026-09-09T17:38:35.474311Z","shell.execute_reply.started":"2026-09-09T17:38:35.436341Z","shell.execute_reply":"2026-09-09T17:38:35.473319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_step17_model():\n    \n    inputs = keras.Input(\n        shape=(3, 8, 1280)\n    )\n    \n    plane_outputs = []\n    \n    for plane_idx in range(3):\n        \n        plane = layers.Lambda(\n            lambda x, i=plane_idx: x[:, i, :, :]\n        )(inputs)\n        \n        # Learn which of the 8 slices matter\n        pooled = SliceAttention(\n            hidden_dim=128\n        )(plane)\n        \n        pooled = layers.LayerNormalization()(pooled)\n        \n        plane_outputs.append(pooled)\n    \n    # Combine Sagittal + Coronal + Axial\n    x = layers.Concatenate()(\n        plane_outputs\n    )\n    \n    x = layers.Dense(\n        256,\n        activation=\"relu\",\n        kernel_regularizer=keras.regularizers.l2(1e-4)\n    )(x)\n    \n    x = layers.BatchNormalization()(x)\n    \n    x = layers.Dropout(0.40)(x)\n    \n    x = layers.Dense(\n        128,\n        activation=\"relu\",\n        kernel_regularizer=keras.regularizers.l2(1e-4)\n    )(x)\n    \n    x = layers.Dropout(0.30)(x)\n    \n    outputs = layers.Dense(\n        12,\n        activation=\"sigmoid\"\n    )(x)\n    \n    model = keras.Model(\n        inputs,\n        outputs\n    )\n    \n    model.compile(\n        optimizer=keras.optimizers.Adam(\n            learning_rate=3e-4\n        ),\n        loss=\"binary_crossentropy\"\n    )\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:39:12.82785Z","iopub.execute_input":"2026-09-09T17:39:12.829037Z","iopub.status.idle":"2026-09-09T17:39:12.836897Z","shell.execute_reply.started":"2026-09-09T17:39:12.828959Z","shell.execute_reply":"2026-09-09T17:39:12.835909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 17 — 5-FOLD CROSS VALIDATION\n# ============================================================\n\nskf = StratifiedKFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\n# Create one value per study for stratification\nstudy_label_counts = Y17.sum(axis=1)\n\n# NumPy median\nmedian_label_count = np.median(study_label_counts)\n\nstratify_labels = (\n    study_label_counts > median_label_count\n).astype(int)\n\nprint(\"Stratification labels:\")\nprint(pd.Series(stratify_labels).value_counts())\n\nfold_scores = []\n\nfor fold, (train_idx, val_idx) in enumerate(\n    skf.split(X17, stratify_labels),\n    start=1\n):\n\n    print(\"\\n\")\n    print(\"=\" * 70)\n    print(f\"STEP 17 — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    tf.keras.backend.clear_session()\n\n    model = build_step17_model()\n\n    callbacks = [\n        keras.callbacks.EarlyStopping(\n            monitor=\"val_loss\",\n            patience=12,\n            restore_best_weights=True\n        ),\n\n        keras.callbacks.ReduceLROnPlateau(\n            monitor=\"val_loss\",\n            factor=0.5,\n            patience=5,\n            min_lr=1e-6\n        )\n    ]\n\n    model.fit(\n        X17[train_idx],\n        Y17[train_idx],\n\n        validation_data=(\n            X17[val_idx],\n            Y17[val_idx]\n        ),\n\n        epochs=80,\n        batch_size=8,\n\n        callbacks=callbacks,\n        verbose=0\n    )\n\n    predictions = model.predict(\n        X17[val_idx],\n        verbose=0\n    )\n\n    aucs = []\n\n    for i, target in enumerate(target_columns):\n\n        y_true = Y17[val_idx, i]\n        y_pred = predictions[:, i]\n\n        # AUC cannot be calculated if validation\n        # contains only one class\n        if len(np.unique(y_true)) < 2:\n            auc = np.nan\n        else:\n            auc = roc_auc_score(\n                y_true,\n                y_pred\n            )\n\n        aucs.append(auc)\n\n        if np.isnan(auc):\n            print(f\"{target:20s}: nan\")\n        else:\n            print(f\"{target:20s}: {auc:.4f}\")\n\n    macro_auc = np.nanmean(aucs)\n\n    fold_scores.append(macro_auc)\n\n    print(f\"\\nFold Macro-AUC: {macro_auc:.4f}\")\n\n\n# ============================================================\n# FINAL STEP 17 RESULT\n# ============================================================\n\nprint(\"\\n\")\nprint(\"=\" * 70)\nprint(\"STEP 17 — LEARNED SLICE AGGREGATION FINAL RESULT\")\nprint(\"=\" * 70)\n\nprint(\"Fold scores:\", fold_scores)\n\nprint(\n    f\"Mean Macro-AUC: {np.mean(fold_scores):.6f}\"\n)\n\nprint(\n    f\"Std: {np.std(fold_scores):.6f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:40:31.8237Z","iopub.execute_input":"2026-09-09T17:40:31.824063Z","iopub.status.idle":"2026-09-09T17:41:39.327238Z","shell.execute_reply.started":"2026-09-09T17:40:31.824037Z","shell.execute_reply":"2026-09-09T17:41:39.32576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 — MRI SERIES ANALYSIS\n# ============================================================\n\nprint(\"=\" * 70)\nprint(\"STEP 18 — SERIES DISTRIBUTION\")\nprint(\"=\" * 70)\n\nprint(\"\\nAll labeled series:\")\nprint(\n    labeled_series[\n        [\"Fluid_Sensitive\", \"Fat_Suppression\", \"Anatomical_Plane\"]\n    ].value_counts()\n)\n\nprint(\"\\n\\nPreferred series:\")\nprint(\n    preferred_series[\n        [\"Fluid_Sensitive\", \"Fat_Suppression\", \"Anatomical_Plane\"]\n    ].value_counts()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:42:28.26454Z","iopub.execute_input":"2026-09-09T17:42:28.264968Z","iopub.status.idle":"2026-09-09T17:42:28.282562Z","shell.execute_reply.started":"2026-09-09T17:42:28.264934Z","shell.execute_reply":"2026-09-09T17:42:28.280898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=\" * 70)\nprint(\"SERIES COUNT PER STUDY\")\nprint(\"=\" * 70)\n\nseries_counts = (\n    labeled_series\n    .groupby(\"StudyInstanceUID\")\n    .size()\n)\n\nprint(series_counts.describe())\n\nprint(\"\\nMinimum series:\", series_counts.min())\nprint(\"Maximum series:\", series_counts.max())\nprint(\"Median series:\", series_counts.median())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:42:39.343131Z","iopub.execute_input":"2026-09-09T17:42:39.343501Z","iopub.status.idle":"2026-09-09T17:42:39.3566Z","shell.execute_reply.started":"2026-09-09T17:42:39.343468Z","shell.execute_reply":"2026-09-09T17:42:39.355142Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=\" * 70)\nprint(\"PLANES PER STUDY\")\nprint(\"=\" * 70)\n\nplane_counts = (\n    labeled_series\n    .groupby(\"StudyInstanceUID\")[\"Anatomical_Plane\"]\n    .nunique()\n)\n\nprint(plane_counts.value_counts().sort_index())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:42:50.689537Z","iopub.execute_input":"2026-09-09T17:42:50.689953Z","iopub.status.idle":"2026-09-09T17:42:50.699455Z","shell.execute_reply.started":"2026-09-09T17:42:50.689925Z","shell.execute_reply":"2026-09-09T17:42:50.697868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 — ADDITIONAL 0/0 SERIES\n# ============================================================\n\nadditional_series = labeled_series[\n    (labeled_series[\"Fluid_Sensitive\"] == 0) &\n    (labeled_series[\"Fat_Suppression\"] == 0)\n].copy()\n\nprint(\"=\" * 70)\nprint(\"ADDITIONAL 0/0 SERIES\")\nprint(\"=\" * 70)\n\nprint(\n    additional_series[\n        [\"Anatomical_Plane\"]\n    ].value_counts()\n)\n\nprint(\"\\nTotal additional series:\", len(additional_series))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:44:00.601185Z","iopub.execute_input":"2026-09-09T17:44:00.601838Z","iopub.status.idle":"2026-09-09T17:44:00.613568Z","shell.execute_reply.started":"2026-09-09T17:44:00.601805Z","shell.execute_reply":"2026-09-09T17:44:00.61256Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"additional_selected = {}\n\nfor study_uid in labeled_uids:\n\n    additional_selected[study_uid] = {}\n\n    study_rows = additional_series[\n        additional_series[\"StudyInstanceUID\"] == study_uid\n    ]\n\n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n        plane_rows = study_rows[\n            study_rows[\"Anatomical_Plane\"] == plane\n        ]\n\n        best_series = None\n        best_count = 0\n\n        for _, row in plane_rows.iterrows():\n\n            files = get_image_files(\n                study_uid,\n                row[\"SeriesInstanceUID\"]\n            )\n\n            if len(files) >= 8 and len(files) > best_count:\n\n                best_series = row[\"SeriesInstanceUID\"]\n                best_count = len(files)\n\n        if best_series is not None:\n            additional_selected[study_uid][plane] = best_series\n\nprint(\"Additional series selection complete.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:44:13.845415Z","iopub.execute_input":"2026-09-09T17:44:13.845766Z","iopub.status.idle":"2026-09-09T17:44:15.319748Z","shell.execute_reply.started":"2026-09-09T17:44:13.845738Z","shell.execute_reply":"2026-09-09T17:44:15.3188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n    count = sum(\n        plane in additional_selected[uid]\n        for uid in labeled_uids\n    )\n\n    print(\n        f\"{plane:10s}: {count}/58 studies\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:44:25.900618Z","iopub.execute_input":"2026-09-09T17:44:25.90102Z","iopub.status.idle":"2026-09-09T17:44:25.907804Z","shell.execute_reply.started":"2026-09-09T17:44:25.900974Z","shell.execute_reply":"2026-09-09T17:44:25.906581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"additional_features = {}\n\nfor study_uid in tqdm(\n    labeled_uids,\n    desc=\"Extracting additional 0/0 features\"\n):\n\n    additional_features[study_uid] = {}\n\n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n        if plane not in additional_selected[study_uid]:\n            continue\n\n        series_uid = additional_selected[\n            study_uid\n        ][plane]\n\n        files = get_image_files(\n            study_uid,\n            series_uid\n        )\n\n        if len(files) < 8:\n            continue\n\n        features = extract_slice_features(files)\n\n        additional_features[\n            study_uid\n        ][plane] = features\n\nprint(\"Additional feature extraction complete.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:44:36.672498Z","iopub.execute_input":"2026-09-09T17:44:36.672857Z","iopub.status.idle":"2026-09-09T17:45:10.607568Z","shell.execute_reply.started":"2026-09-09T17:44:36.672827Z","shell.execute_reply":"2026-09-09T17:45:10.606577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for study_uid in list(additional_features.keys())[:3]:\n\n    print(\"\\nStudy:\", study_uid)\n\n    for plane, features in additional_features[\n        study_uid\n    ].items():\n\n        print(\n            plane,\n            features.shape\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:45:36.286937Z","iopub.execute_input":"2026-09-09T17:45:36.287361Z","iopub.status.idle":"2026-09-09T17:45:36.293719Z","shell.execute_reply.started":"2026-09-09T17:45:36.287329Z","shell.execute_reply":"2026-09-09T17:45:36.292483Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 — BUILD MULTI-SEQUENCE DATASET\n# ============================================================\n\nX18 = []\nY18 = []\n\nfor _, row in labeled_data.iterrows():\n\n    study_uid = row[\"StudyInstanceUID\"]\n\n    study_data = []\n    study_mask = []\n\n    # --------------------------------------------------------\n    # First 3 = preferred 1/1 sequences\n    # Next 3  = additional 0/0 sequences\n    #\n    # Order:\n    # 0 Sagittal preferred\n    # 1 Coronal preferred\n    # 2 Axial preferred\n    # 3 Sagittal additional\n    # 4 Coronal additional\n    # 5 Axial additional\n    # --------------------------------------------------------\n\n    # Preferred sequences\n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n        if plane in multi17_features[study_uid]:\n\n            study_data.append(\n                multi17_features[study_uid][plane]\n            )\n\n            study_mask.append(1.0)\n\n        else:\n\n            study_data.append(\n                np.zeros(\n                    (8, 1280),\n                    dtype=np.float32\n                )\n            )\n\n            study_mask.append(0.0)\n\n    # Additional 0/0 sequences\n    for plane in [\"Sagittal\", \"Coronal\", \"Axial\"]:\n\n        if plane in additional_features[study_uid]:\n\n            study_data.append(\n                additional_features[study_uid][plane]\n            )\n\n            study_mask.append(1.0)\n\n        else:\n\n            study_data.append(\n                np.zeros(\n                    (8, 1280),\n                    dtype=np.float32\n                )\n            )\n\n            study_mask.append(0.0)\n\n    X18.append(\n        np.stack(study_data)\n    )\n\n    Y18.append(\n        row[target_columns].values.astype(\n            np.float32\n        )\n    )\n\nX18 = np.stack(X18)\nY18 = np.stack(Y18)\n\nprint(\"X18 shape:\", X18.shape)\nprint(\"Y18 shape:\", Y18.shape)\nprint(\"NaN:\", np.isnan(X18).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:46:47.805355Z","iopub.execute_input":"2026-09-09T17:46:47.80577Z","iopub.status.idle":"2026-09-09T17:46:47.847065Z","shell.execute_reply.started":"2026-09-09T17:46:47.805733Z","shell.execute_reply":"2026-09-09T17:46:47.845302Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# SEQUENCE AVAILABILITY\n# ============================================================\n\nsequence_names = [\n    \"Preferred Sagittal\",\n    \"Preferred Coronal\",\n    \"Preferred Axial\",\n    \"Additional Sagittal\",\n    \"Additional Coronal\",\n    \"Additional Axial\"\n]\n\nprint(\"=\" * 70)\nprint(\"SEQUENCE AVAILABILITY\")\nprint(\"=\" * 70)\n\nfor i, name in enumerate(sequence_names):\n\n    available = np.sum(\n        np.any(X18[:, i] != 0, axis=(1, 2))\n    )\n\n    print(\n        f\"{name:22s}: {available}/58\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:46:58.593878Z","iopub.execute_input":"2026-09-09T17:46:58.59425Z","iopub.status.idle":"2026-09-09T17:46:58.602931Z","shell.execute_reply.started":"2026-09-09T17:46:58.594223Z","shell.execute_reply":"2026-09-09T17:46:58.602216Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 MODEL\n# ============================================================\n\ndef build_step18_model():\n\n    inputs = keras.Input(\n        shape=(6, 8, 1280),\n        name=\"mri_features\"\n    )\n\n    sequence_outputs = []\n\n    for seq_idx in range(6):\n\n        seq = layers.Lambda(\n            lambda x, i=seq_idx:\n            x[:, i, :, :]\n        )(inputs)\n\n        # Slice aggregation\n        seq = SliceAttention(\n            hidden_dim=64\n        )(seq)\n\n        seq = layers.LayerNormalization()(seq)\n\n        seq = layers.Dense(\n            256,\n            activation=\"relu\"\n        )(seq)\n\n        seq = layers.Dropout(0.25)(seq)\n\n        sequence_outputs.append(seq)\n\n    # --------------------------------------------------------\n    # Concatenate all sequence representations\n    # --------------------------------------------------------\n\n    x = layers.Concatenate()(\n        sequence_outputs\n    )\n\n    x = layers.Dense(\n        256,\n        activation=\"relu\",\n        kernel_regularizer=keras.regularizers.l2(1e-4)\n    )(x)\n\n    x = layers.BatchNormalization()(x)\n\n    x = layers.Dropout(0.40)(x)\n\n    x = layers.Dense(\n        128,\n        activation=\"relu\",\n        kernel_regularizer=keras.regularizers.l2(1e-4)\n    )(x)\n\n    x = layers.Dropout(0.30)(x)\n\n    outputs = layers.Dense(\n        12,\n        activation=\"sigmoid\"\n    )(x)\n\n    model = keras.Model(\n        inputs,\n        outputs\n    )\n\n    model.compile(\n        optimizer=keras.optimizers.Adam(\n            learning_rate=2e-4\n        ),\n        loss=\"binary_crossentropy\"\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:47:11.10148Z","iopub.execute_input":"2026-09-09T17:47:11.101825Z","iopub.status.idle":"2026-09-09T17:47:11.110521Z","shell.execute_reply.started":"2026-09-09T17:47:11.101798Z","shell.execute_reply":"2026-09-09T17:47:11.109628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 — 5-FOLD CROSS VALIDATION\n# ============================================================\n\nskf = StratifiedKFold(\n    n_splits=5,\n    shuffle=True,\n    random_state=42\n)\n\nstudy_label_counts = Y18.sum(axis=1)\n\nmedian_label_count = np.median(\n    study_label_counts\n)\n\nstratify_labels = (\n    study_label_counts > median_label_count\n).astype(int)\n\nprint(\"Stratification labels:\")\nprint(\n    pd.Series(\n        stratify_labels\n    ).value_counts()\n)\n\nfold_scores = []\n\nfor fold, (train_idx, val_idx) in enumerate(\n    skf.split(X18, stratify_labels),\n    start=1\n):\n\n    print(\"\\n\")\n    print(\"=\" * 70)\n    print(f\"STEP 18 — FOLD {fold}\")\n    print(\"=\" * 70)\n\n    tf.keras.backend.clear_session()\n\n    model = build_step18_model()\n\n    callbacks = [\n\n        keras.callbacks.EarlyStopping(\n            monitor=\"val_loss\",\n            patience=12,\n            restore_best_weights=True\n        ),\n\n        keras.callbacks.ReduceLROnPlateau(\n            monitor=\"val_loss\",\n            factor=0.5,\n            patience=5,\n            min_lr=1e-6\n        )\n    ]\n\n    model.fit(\n        X18[train_idx],\n        Y18[train_idx],\n\n        validation_data=(\n            X18[val_idx],\n            Y18[val_idx]\n        ),\n\n        epochs=80,\n        batch_size=8,\n\n        callbacks=callbacks,\n        verbose=0\n    )\n\n    predictions = model.predict(\n        X18[val_idx],\n        verbose=0\n    )\n\n    aucs = []\n\n    for i, target in enumerate(\n        target_columns\n    ):\n\n        y_true = Y18[val_idx, i]\n        y_pred = predictions[:, i]\n\n        if len(np.unique(y_true)) < 2:\n\n            auc = np.nan\n\n        else:\n\n            auc = roc_auc_score(\n                y_true,\n                y_pred\n            )\n\n        aucs.append(auc)\n\n        if np.isnan(auc):\n\n            print(\n                f\"{target:20s}: nan\"\n            )\n\n        else:\n\n            print(\n                f\"{target:20s}: {auc:.4f}\"\n            )\n\n    macro_auc = np.nanmean(aucs)\n\n    fold_scores.append(\n        macro_auc\n    )\n\n    print(\n        f\"\\nFold Macro-AUC: {macro_auc:.4f}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:47:23.138653Z","iopub.execute_input":"2026-09-09T17:47:23.139022Z","iopub.status.idle":"2026-09-09T17:49:08.648277Z","shell.execute_reply.started":"2026-09-09T17:47:23.138976Z","shell.execute_reply":"2026-09-09T17:49:08.647567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# STEP 18 FINAL RESULT\n# ============================================================\n\nprint(\"\\n\")\nprint(\"=\" * 70)\nprint(\"STEP 18 — MULTI-SEQUENCE FINAL RESULT\")\nprint(\"=\" * 70)\n\nprint(\n    \"Fold scores:\",\n    fold_scores\n)\n\nprint(\n    f\"Mean Macro-AUC: \"\n    f\"{np.mean(fold_scores):.6f}\"\n)\n\nprint(\n    f\"Std: \"\n    f\"{np.std(fold_scores):.6f}\"\n)\n\nprint(\"\\n\")\nprint(\"BENCHMARK\")\nprint(\"-\" * 70)\nprint(\"Step 16 — 8-slice multi-series : 0.627379\")\nprint(\n    f\"Step 18 — multi-sequence       : \"\n    f\"{np.mean(fold_scores):.6f}\"\n)\nprint(\n    f\"Improvement                    : \"\n    f\"{np.mean(fold_scores) - 0.627379:+.6f}\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:49:26.408841Z","iopub.execute_input":"2026-09-09T17:49:26.410102Z","iopub.status.idle":"2026-09-09T17:49:26.419822Z","shell.execute_reply.started":"2026-09-09T17:49:26.410018Z","shell.execute_reply":"2026-09-09T17:49:26.418576Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 💾 SAVE EVERYTHING IMPORTANT — BEFORE CLOSING KAGGLE\n# ============================================================\n\nimport os\nimport numpy as np\nimport pandas as pd\n\nSAVE_DIR = \"/kaggle/working/RSNA_Step18_Backup\"\nos.makedirs(SAVE_DIR, exist_ok=True)\n\nprint(\"=\" * 70)\nprint(\"SAVING STEP 18 PROJECT FILES\")\nprint(\"=\" * 70)\n\n# ------------------------------------------------------------\n# 1. SAVE MAIN FEATURES\n# ------------------------------------------------------------\n\nif \"X18\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"X18.npy\"), X18)\n    print(\"✅ X18.npy saved\")\n\nif \"Y18\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"Y18.npy\"), Y18)\n    print(\"✅ Y18.npy saved\")\n\n\n# ------------------------------------------------------------\n# 2. SAVE PREVIOUS STEP 17 FEATURES IF THEY EXIST\n# ------------------------------------------------------------\n\nif \"X17\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"X17.npy\"), X17)\n    print(\"✅ X17.npy saved\")\n\nif \"Y17\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"Y17.npy\"), Y17)\n    print(\"✅ Y17.npy saved\")\n\n\n# ------------------------------------------------------------\n# 3. SAVE STEP 16 FEATURES IF THEY EXIST\n# ------------------------------------------------------------\n\nif \"X\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"X_step16.npy\"), X)\n    print(\"✅ X_step16.npy saved\")\n\nif \"Y\" in globals():\n    np.save(os.path.join(SAVE_DIR, \"Y_step16.npy\"), Y)\n    print(\"✅ Y_step16.npy saved\")\n\n\n# ------------------------------------------------------------\n# 4. SAVE TRAINING DATA\n# ------------------------------------------------------------\n\nif \"train\" in globals():\n    train.to_csv(\n        os.path.join(SAVE_DIR, \"train_backup.csv\"),\n        index=False\n    )\n    print(\"✅ train_backup.csv saved\")\n\nif \"train_series\" in globals():\n    train_series.to_csv(\n        os.path.join(SAVE_DIR, \"train_series_backup.csv\"),\n        index=False\n    )\n    print(\"✅ train_series_backup.csv saved\")\n\n\n# ------------------------------------------------------------\n# 5. SAVE TEST DATA\n# ------------------------------------------------------------\n\nif \"test\" in globals():\n    test.to_csv(\n        os.path.join(SAVE_DIR, \"test_backup.csv\"),\n        index=False\n    )\n    print(\"✅ test_backup.csv saved\")\n\nif \"test_series\" in globals():\n    test_series.to_csv(\n        os.path.join(SAVE_DIR, \"test_series_backup.csv\"),\n        index=False\n    )\n    print(\"✅ test_series_backup.csv saved\")\n\nif \"sample_submission\" in globals():\n    sample_submission.to_csv(\n        os.path.join(SAVE_DIR, \"sample_submission_backup.csv\"),\n        index=False\n    )\n    print(\"✅ sample_submission_backup.csv saved\")\n\n\n# ------------------------------------------------------------\n# 6. SAVE STEP 18 MODEL IF A MODEL EXISTS\n# ------------------------------------------------------------\n\nif \"model\" in globals():\n\n    try:\n        model.save(\n            os.path.join(SAVE_DIR, \"latest_model.keras\")\n        )\n        print(\"✅ latest_model.keras saved\")\n    except Exception as e:\n        print(\"⚠️ Could not save 'model':\", e)\n\n\n# ------------------------------------------------------------\n# 7. SAVE IMPORTANT EXPERIMENT RESULTS\n# ------------------------------------------------------------\n\nresults = {\n    \"Step\": [\n        \"Step 16\",\n        \"Step 17\",\n        \"Step 18\"\n    ],\n    \"Model\": [\n        \"8-slice multi-series\",\n        \"Learned slice attention\",\n        \"Multi-sequence\"\n    ],\n    \"Mean_Macro_AUC\": [\n        0.627379,\n        0.619222,\n        0.648255\n    ],\n    \"Std\": [\n        0.058722,\n        0.077529,\n        0.044772\n    ]\n}\n\nresults_df = pd.DataFrame(results)\n\nresults_df.to_csv(\n    os.path.join(SAVE_DIR, \"experiment_results.csv\"),\n    index=False\n)\n\nprint(\"✅ experiment_results.csv saved\")\n\n\n# ------------------------------------------------------------\n# 8. SAVE A TEXT SUMMARY\n# ------------------------------------------------------------\n\nsummary = \"\"\"\n============================================================\nRSNA KNEE ABNORMALITY DETECTION — PROJECT BACKUP\n============================================================\n\nCURRENT CHAMPION\nStep 18 — Multi-sequence model\n\nMean Macro-AUC : 0.648255\nStd            : 0.044772\n\nFold Scores:\n0.583803\n0.629618\n0.631459\n0.709392\n0.687004\n\n------------------------------------------------------------\nBENCHMARK\n------------------------------------------------------------\n\nStep 16 — 8-slice multi-series : 0.627379\nStep 17 — Slice attention      : 0.619222\nStep 18 — Multi-sequence       : 0.648255\n\nStep 18 improvement over Step 16:\n+0.020876\n\n------------------------------------------------------------\nNEXT EXPERIMENT\n------------------------------------------------------------\n\nStep 19:\n6-sequence model with simple mean aggregation.\n\nPurpose:\nDetermine whether the improvement comes from the\nadditional MRI sequences or from the attention architecture.\n\n============================================================\n\"\"\"\n\nwith open(\n    os.path.join(SAVE_DIR, \"PROJECT_STATUS.txt\"),\n    \"w\"\n) as f:\n    f.write(summary)\n\nprint(\"✅ PROJECT_STATUS.txt saved\")\n\n\n# ------------------------------------------------------------\n# 9. SHOW EVERYTHING THAT WAS SAVED\n# ------------------------------------------------------------\n\nprint(\"\\n\" + \"=\" * 70)\nprint(\"📁 BACKUP CONTENTS\")\nprint(\"=\" * 70)\n\ntotal_size = 0\n\nfor root, dirs, files in os.walk(SAVE_DIR):\n\n    for file in sorted(files):\n\n        path = os.path.join(root, file)\n        size_mb = os.path.getsize(path) / (1024 ** 2)\n        total_size += size_mb\n\n        print(f\"{file:<40} {size_mb:>10.2f} MB\")\n\nprint(\"-\" * 70)\nprint(f\"TOTAL BACKUP SIZE: {total_size:.2f} MB\")\nprint(\"=\" * 70)\n\nprint(\"\\n🎉 BACKUP COMPLETE!\")\nprint(\"Download/save the important files from /kaggle/working/RSNA_Step18_Backup\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-09T17:58:14.305795Z","iopub.execute_input":"2026-09-09T17:58:14.306189Z","iopub.status.idle":"2026-09-09T17:58:14.79025Z","shell.execute_reply.started":"2026-09-09T17:58:14.306147Z","shell.execute_reply":"2026-09-09T17:58:14.788228Z"}},"outputs":[],"execution_count":null}]}