{"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":"from pathlib import Path\nimport pandas as pd\n\nINPUT = Path(\"/kaggle/input\")\n\n# Find the competition directory without assuming its exact folder name\nsample_path = next(INPUT.rglob(\"sample_submission.csv\"))\nDATA = sample_path.parent\n\nprint(\"Competition data:\", DATA)\n\ntrain = pd.read_csv(DATA / \"train.csv\")\ntrain_series = pd.read_csv(DATA / \"train_series.csv\")\nsample_submission = pd.read_csv(sample_path)\n\nprint(\"Training studies:\", len(train))\nprint(\"Training series:\", len(train_series))\nprint(\"Submission rows:\", len(sample_submission))\n\ndisplay(train.head())\ndisplay(train_series.head())\ndisplay(sample_submission.head())","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2026-09-13T12:59:39.349269Z","iopub.execute_input":"2026-09-13T12:59:39.350057Z","iopub.status.idle":"2026-09-13T12:59:39.506857Z","shell.execute_reply.started":"2026-09-13T12:59:39.350015Z","shell.execute_reply":"2026-09-13T12:59:39.50623Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"LABELS = [\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\nlabel_summary = pd.DataFrame({\n    \"labelled_studies\": train[LABELS].notna().sum(),\n    \"positive_studies\": train[LABELS].eq(1).sum(),\n    \"negative_studies\": train[LABELS].eq(0).sum(),\n})\n\ndisplay(label_summary)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T12:59:49.061979Z","iopub.execute_input":"2026-09-13T12:59:49.06229Z","iopub.status.idle":"2026-09-13T12:59:49.076235Z","shell.execute_reply.started":"2026-09-13T12:59:49.062264Z","shell.execute_reply":"2026-09-13T12:59:49.07525Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = sample_submission.copy()\n\nexpected_columns = [\"StudyInstanceUID\"] + LABELS\nassert submission.columns.tolist() == expected_columns\n\n# Confirm all predictions are present and between 0 and 1\npredictions = submission[LABELS]\nassert predictions.notna().all().all()\nassert ((predictions >= 0) & (predictions <= 1)).all().all()\n\nsubmission.to_csv(\n    \"/kaggle/working/submission.csv\",\n    index=False,\n)\n\ndisplay(submission.head())\nprint(\"Created /kaggle/working/submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:03:29.227598Z","iopub.execute_input":"2026-09-13T13:03:29.227942Z","iopub.status.idle":"2026-09-13T13:03:29.254206Z","shell.execute_reply.started":"2026-09-13T13:03:29.227915Z","shell.execute_reply":"2026-09-13T13:03:29.253614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"label_stats = pd.DataFrame({\n    \"labelled\": train[LABELS].notna().sum(),\n    \"positive\": train[LABELS].eq(1).sum(),\n    \"negative\": train[LABELS].eq(0).sum(),\n})\n\nlabel_stats[\"positive_rate\"] = (\n    label_stats[\"positive\"] / label_stats[\"labelled\"]\n)\n\ndisplay(label_stats.sort_values(\"positive_rate\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:20:03.429569Z","iopub.execute_input":"2026-09-13T13:20:03.430414Z","iopub.status.idle":"2026-09-13T13:20:03.451277Z","shell.execute_reply.started":"2026-09-13T13:20:03.430376Z","shell.execute_reply":"2026-09-13T13:20:03.450349Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"complete_gold = train[LABELS].notna().all(axis=1)\nany_gold = train[LABELS].notna().any(axis=1)\n\nprint(\"Total training studies:\", len(train))\nprint(\"Studies with all 12 labels:\", int(complete_gold.sum()))\nprint(\"Studies with any labels:\", int(any_gold.sum()))\nprint(\n    \"Partially labelled studies:\",\n    int((any_gold & ~complete_gold).sum())\n)\nprint(\"Completely unlabelled:\", int((~any_gold).sum()))\nprint(\"Studies with reports:\", int(train[\"Report\"].notna().sum()))\n\nreport_lengths = (\n    train[\"Report\"]\n    .fillna(\"\")\n    .astype(str)\n    .str.len()\n)\n\nprint(\"\\nReport-length summary:\")\ndisplay(\n    report_lengths.describe(\n        percentiles=[0.10, 0.25, 0.50, 0.75, 0.90]\n    ).round(1)\n)\n\nseries_per_study = (\n    train_series\n    .groupby(\"StudyInstanceUID\")\n    .size()\n)\n\nprint(\"\\nSeries per study:\")\ndisplay(\n    series_per_study.describe(\n        percentiles=[0.10, 0.25, 0.50, 0.75, 0.90]\n    ).round(1)\n)\n\nprint(\"\\nSeries by anatomical plane:\")\ndisplay(\n    train_series[\"Anatomical_Plane\"]\n    .value_counts(dropna=False)\n    .to_frame(\"series\")\n)\n\nprint(\"\\nSequence combinations:\")\ndisplay(\n    train_series.groupby(\n        [\"Anatomical_Plane\", \"Fluid_Sensitive\", \"Fat_Suppression\"],\n        dropna=False,\n    )\n    .size()\n    .sort_values(ascending=False)\n    .to_frame(\"series\")\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:22:11.895937Z","iopub.execute_input":"2026-09-13T13:22:11.896651Z","iopub.status.idle":"2026-09-13T13:22:11.959291Z","shell.execute_reply.started":"2026-09-13T13:22:11.896623Z","shell.execute_reply":"2026-09-13T13:22:11.958321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib.util\nimport numpy as np\nimport torch\n\nseries = train_series.copy()\n\nseries[\"Sequence_Type\"] = np.select(\n    [\n        (series[\"Fluid_Sensitive\"] == 1)\n        & (series[\"Fat_Suppression\"] == 1),\n\n        (series[\"Fluid_Sensitive\"] == 0)\n        & (series[\"Fat_Suppression\"] == 0),\n    ],\n    [\n        \"Fluid+FatSupp\",\n        \"Neither\",\n    ],\n    default=\"Mixed\",\n)\n\nseries[\"Slot\"] = (\n    series[\"Anatomical_Plane\"].astype(str)\n    + \" | \"\n    + series[\"Sequence_Type\"]\n)\n\nunique_slots = series.drop_duplicates(\n    [\"StudyInstanceUID\", \"Slot\"]\n)\n\nslot_coverage = (\n    unique_slots.groupby(\"Slot\")[\"StudyInstanceUID\"]\n    .nunique()\n    .to_frame(\"studies\")\n)\n\nslot_coverage[\"coverage_pct\"] = (\n    100 * slot_coverage[\"studies\"] / len(train)\n)\n\nslot_coverage[\"total_series\"] = (\n    series[\"Slot\"].value_counts()\n)\n\nslot_coverage = slot_coverage.sort_values(\n    \"coverage_pct\",\n    ascending=False,\n)\n\ndisplay(slot_coverage.round(1))\n\nprint(\"PyTorch version:\", torch.__version__)\nprint(\"CUDA available:\", torch.cuda.is_available())\n\nif torch.cuda.is_available():\n    print(\"GPU:\", torch.cuda.get_device_name(0))\n    print(\n        \"GPU memory:\",\n        round(\n            torch.cuda.get_device_properties(0).total_memory\n            / 1024**3,\n            1,\n        ),\n        \"GB\",\n    )\n\nprint(\"pydicom installed:\", importlib.util.find_spec(\"pydicom\") is not None)\nprint(\"timm installed:\", importlib.util.find_spec(\"timm\") is not None)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:29:17.340759Z","iopub.execute_input":"2026-09-13T13:29:17.341147Z","iopub.status.idle":"2026-09-13T13:29:23.144107Z","shell.execute_reply.started":"2026-09-13T13:29:17.341119Z","shell.execute_reply":"2026-09-13T13:29:23.143399Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\nfrom tqdm.auto import tqdm\nimport pandas as pd\nimport numpy as np\nimport os\n\n# Reload paths and tables so the cell remains self-contained\nINPUT = Path(\"/kaggle/input\")\nsample_path = next(INPUT.rglob(\"sample_submission.csv\"))\nDATA = sample_path.parent\nSERIES_ROOT = DATA / \"train_series\"\n\ntrain = pd.read_csv(DATA / \"train.csv\")\ntrain_series = pd.read_csv(DATA / \"train_series.csv\")\n\nseries = train_series.copy()\n\nseries[\"Sequence_Type\"] = np.select(\n    [\n        (series[\"Fluid_Sensitive\"] == 1)\n        & (series[\"Fat_Suppression\"] == 1),\n\n        (series[\"Fluid_Sensitive\"] == 0)\n        & (series[\"Fat_Suppression\"] == 0),\n    ],\n    [\n        \"Fluid+FatSupp\",\n        \"Neither\",\n    ],\n    default=\"Mixed\",\n)\n\nseries[\"Slot\"] = (\n    series[\"Anatomical_Plane\"].astype(str)\n    + \" | \"\n    + series[\"Sequence_Type\"]\n)\n\nTARGET_SLOTS = [\n    \"Axial | Fluid+FatSupp\",\n    \"Sagittal | Neither\",\n    \"Coronal | Fluid+FatSupp\",\n    \"Sagittal | Fluid+FatSupp\",\n]\n\ncandidates = series[\n    series[\"Slot\"].isin(TARGET_SLOTS)\n].copy()\n\n# Identify study-slot combinations containing multiple series\ncandidates[\"group_size\"] = candidates.groupby(\n    [\"StudyInstanceUID\", \"Slot\"]\n)[\"SeriesInstanceUID\"].transform(\"size\")\n\n# Slice counts are only needed when there is a choice\ncandidates[\"slice_count\"] = -1\n\nduplicate_indices = candidates.index[\n    candidates[\"group_size\"] > 1\n]\n\nfor idx in tqdm(\n    duplicate_indices,\n    desc=\"Counting duplicate-series slices\",\n):\n    row = candidates.loc[idx]\n\n    folder = (\n        SERIES_ROOT\n        / str(row[\"StudyInstanceUID\"])\n        / str(row[\"SeriesInstanceUID\"])\n    )\n\n    try:\n        with os.scandir(folder) as entries:\n            count = sum(\n                entry.name.lower().endswith(\".dcm\")\n                for entry in entries\n            )\n    except FileNotFoundError:\n        count = 0\n\n    candidates.loc[idx, \"slice_count\"] = count\n\n# Prefer the largest series; use UID as a deterministic tie-breaker\nselected = (\n    candidates.sort_values(\n        [\n            \"StudyInstanceUID\",\n            \"Slot\",\n            \"slice_count\",\n            \"SeriesInstanceUID\",\n        ],\n        ascending=[True, True, False, True],\n    )\n    .drop_duplicates(\n        [\"StudyInstanceUID\", \"Slot\"],\n        keep=\"first\",\n    )\n    .reset_index(drop=True)\n)\n\nselected.to_csv(\n    \"/kaggle/working/selected_series.csv\",\n    index=False,\n)\n\nsummary = (\n    selected.groupby(\"Slot\")\n    .size()\n    .reindex(TARGET_SLOTS)\n    .to_frame(\"selected_studies\")\n)\n\nsummary[\"coverage_pct\"] = (\n    100 * summary[\"selected_studies\"] / len(train)\n)\n\nsummary[\"missing_studies\"] = (\n    len(train) - summary[\"selected_studies\"]\n)\n\nduplicate_groups = (\n    candidates.loc[\n        candidates[\"group_size\"] > 1,\n        [\"StudyInstanceUID\", \"Slot\"],\n    ]\n    .drop_duplicates()\n    .shape[0]\n)\n\ndisplay(summary.round(1))\n\nprint(\"Candidate series:\", len(candidates))\nprint(\"Selected series:\", len(selected))\nprint(\"Duplicate study-slot groups resolved:\", duplicate_groups)\nprint(\n    \"Saved:\",\n    \"/kaggle/working/selected_series.csv\",\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:35:05.669825Z","iopub.execute_input":"2026-09-13T13:35:05.670498Z","iopub.status.idle":"2026-09-13T13:36:00.819105Z","shell.execute_reply.started":"2026-09-13T13:35:05.67047Z","shell.execute_reply":"2026-09-13T13:36:00.818395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport pydicom\n\n# Locate the competition data.\nINPUT = Path(\"/kaggle/input\")\nsample_path = next(INPUT.rglob(\"sample_submission.csv\"))\nDATA = sample_path.parent\nSERIES_ROOT = DATA / \"train_series\"\n\nselected = pd.read_csv(\n    \"/kaggle/working/selected_series.csv\",\n    dtype={\n        \"StudyInstanceUID\": str,\n        \"SeriesInstanceUID\": str,\n    },\n)\n\nslots = [\n    \"Axial | Fluid+FatSupp\",\n    \"Sagittal | Neither\",\n    \"Coronal | Fluid+FatSupp\",\n    \"Sagittal | Fluid+FatSupp\",\n]\n\n# Select one example from each slot.\nexamples = pd.concat(\n    [\n        selected[selected[\"Slot\"] == slot].sample(\n            n=1,\n            random_state=2026 + index,\n        )\n        for index, slot in enumerate(slots)\n    ],\n    ignore_index=True,\n)\n\n\ndef decode_example(row):\n    folder = (\n        SERIES_ROOT\n        / row[\"StudyInstanceUID\"]\n        / row[\"SeriesInstanceUID\"]\n    )\n\n    files = sorted(folder.glob(\"*.dcm\"))\n\n    if not files:\n        raise FileNotFoundError(\"No DICOM files found\")\n\n    # Any slice is sufficient for this decoder smoke test.\n    path = files[len(files) // 2]\n    ds = pydicom.dcmread(path, force=True)\n\n    image = np.asarray(\n        ds.pixel_array,\n        dtype=np.float32,\n    ).squeeze()\n\n    if image.ndim != 2:\n        raise ValueError(f\"Unexpected shape: {image.shape}\")\n\n    slope = float(getattr(ds, \"RescaleSlope\", 1))\n    intercept = float(getattr(ds, \"RescaleIntercept\", 0))\n    image = image * slope + intercept\n\n    low, high = np.percentile(image, [1, 99])\n\n    if high > low:\n        image = np.clip(\n            (image - low) / (high - low),\n            0,\n            1,\n        )\n\n    if getattr(ds, \"PhotometricInterpretation\", \"\") == \"MONOCHROME1\":\n        image = 1 - image\n\n    return image, len(files)\n\n\nfig, axes = plt.subplots(2, 2, figsize=(10, 9))\nsuccesses = 0\nerrors = []\n\nfor axis, (_, row) in zip(axes.flat, examples.iterrows()):\n    try:\n        image, slice_count = decode_example(row)\n\n        axis.imshow(image, cmap=\"gray\")\n        axis.set_title(\n            f\"{row['Slot']}\\n\"\n            f\"{slice_count} slices, shape {image.shape}\"\n        )\n        successes += 1\n\n    except Exception as error:\n        errors.append(f\"{row['Slot']}: {error}\")\n        axis.text(\n            0.5,\n            0.5,\n            f\"{type(error).__name__}\\n{error}\",\n            ha=\"center\",\n            va=\"center\",\n            wrap=True,\n        )\n        axis.set_title(row[\"Slot\"])\n\n    axis.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n\nprint(f\"Successfully decoded: {successes}/4\")\n\nif errors:\n    print(\"\\nErrors:\")\n    for error in errors:\n        print(\"-\", error)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-13T13:42:42.235247Z","iopub.execute_input":"2026-09-13T13:42:42.236138Z","iopub.status.idle":"2026-09-13T13:42:43.813497Z","shell.execute_reply.started":"2026-09-13T13:42:42.236095Z","shell.execute_reply":"2026-09-13T13:42:43.812752Z"}},"outputs":[],"execution_count":null}]}