|
@@ -285,32 +285,14 @@ def initalize_dataloaders(
|
|
|
return loaders
|
|
return loaders
|
|
|
|
|
|
|
|
|
|
|
|
|
-def divide_dataset_by_patient_id(
|
|
|
|
|
- dataset: ADNIDataset,
|
|
|
|
|
- ptids: List[Tuple[int, str]],
|
|
|
|
|
- ratios: Tuple[float, float, float],
|
|
|
|
|
- seed: int,
|
|
|
|
|
-) -> List[data.Subset[ADNIDataset]]:
|
|
|
|
|
- """
|
|
|
|
|
- Divides the dataset into training, validation, and test sets based on patient IDs.
|
|
|
|
|
- Ensures that all samples from the same patient are in the same set.
|
|
|
|
|
|
|
+def _patient_index(
|
|
|
|
|
+ dataset: ADNIDataset, ptids: List[Tuple[int, str]]
|
|
|
|
|
+) -> Dict[str, List[int]]:
|
|
|
|
|
+ """Map patient id -> dataset sample indices, validating PTID consistency.
|
|
|
|
|
|
|
|
- Args:
|
|
|
|
|
- dataset (ADNIDataset): The dataset to divide.
|
|
|
|
|
- ptids (List[Tuple[int, str]]): A list of tuples containing image file IDs and their corresponding patient IDs.
|
|
|
|
|
- ratios (Tuple[float, float, float]): The ratios for training, validation, and test sets.
|
|
|
|
|
- seed (int): The random seed for reproducibility.
|
|
|
|
|
-
|
|
|
|
|
- Returns:
|
|
|
|
|
- List[data.Subset[ADNIDataset]]: A list of subsets for training, validation, and test sets.
|
|
|
|
|
-
|
|
|
|
|
- Notes:
|
|
|
|
|
- This split is grouped by PTID, so all images from the same patient are assigned
|
|
|
|
|
- to exactly one partition to avoid patient-level leakage across train/val/test.
|
|
|
|
|
|
|
+ Shared by the single-split and k-fold partitioners so both enforce the same
|
|
|
|
|
+ patient-grouping and leakage checks.
|
|
|
"""
|
|
"""
|
|
|
- if not math.isclose(sum(ratios), 1.0, rel_tol=1e-9, abs_tol=1e-9):
|
|
|
|
|
- raise ValueError(f"Ratios must sum to 1.0, got {ratios} (sum={sum(ratios)}).")
|
|
|
|
|
-
|
|
|
|
|
if not ptids:
|
|
if not ptids:
|
|
|
raise ValueError("ptids list cannot be empty.")
|
|
raise ValueError("ptids list cannot be empty.")
|
|
|
|
|
|
|
@@ -320,7 +302,6 @@ def divide_dataset_by_patient_id(
|
|
|
patient_id_str = str(patient_id).strip()
|
|
patient_id_str = str(patient_id).strip()
|
|
|
if not patient_id_str or patient_id_str.lower() == "nan":
|
|
if not patient_id_str or patient_id_str.lower() == "nan":
|
|
|
raise ValueError(f"Invalid PTID for Image Data ID {image_id_int}.")
|
|
raise ValueError(f"Invalid PTID for Image Data ID {image_id_int}.")
|
|
|
-
|
|
|
|
|
if (
|
|
if (
|
|
|
image_id_int in image_to_patient
|
|
image_id_int in image_to_patient
|
|
|
and image_to_patient[image_id_int] != patient_id_str
|
|
and image_to_patient[image_id_int] != patient_id_str
|
|
@@ -329,7 +310,6 @@ def divide_dataset_by_patient_id(
|
|
|
f"Conflicting PTIDs for Image Data ID {image_id_int}: "
|
|
f"Conflicting PTIDs for Image Data ID {image_id_int}: "
|
|
|
f"{image_to_patient[image_id_int]} vs {patient_id_str}."
|
|
f"{image_to_patient[image_id_int]} vs {patient_id_str}."
|
|
|
)
|
|
)
|
|
|
-
|
|
|
|
|
image_to_patient[image_id_int] = patient_id_str
|
|
image_to_patient[image_id_int] = patient_id_str
|
|
|
|
|
|
|
|
patient_to_indices: Dict[str, List[int]] = {}
|
|
patient_to_indices: Dict[str, List[int]] = {}
|
|
@@ -338,11 +318,38 @@ def divide_dataset_by_patient_id(
|
|
|
raise ValueError(
|
|
raise ValueError(
|
|
|
f"Missing PTID mapping for dataset Image Data ID {image_id}."
|
|
f"Missing PTID mapping for dataset Image Data ID {image_id}."
|
|
|
)
|
|
)
|
|
|
|
|
+ patient_to_indices.setdefault(image_to_patient[image_id], []).append(idx)
|
|
|
|
|
+
|
|
|
|
|
+ return patient_to_indices
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def divide_dataset_by_patient_id(
|
|
|
|
|
+ dataset: ADNIDataset,
|
|
|
|
|
+ ptids: List[Tuple[int, str]],
|
|
|
|
|
+ ratios: Tuple[float, float, float],
|
|
|
|
|
+ seed: int,
|
|
|
|
|
+) -> List[data.Subset[ADNIDataset]]:
|
|
|
|
|
+ """
|
|
|
|
|
+ Divides the dataset into training, validation, and test sets based on patient IDs.
|
|
|
|
|
+ Ensures that all samples from the same patient are in the same set.
|
|
|
|
|
|
|
|
- patient_id = image_to_patient[image_id]
|
|
|
|
|
- if patient_id not in patient_to_indices:
|
|
|
|
|
- patient_to_indices[patient_id] = []
|
|
|
|
|
- patient_to_indices[patient_id].append(idx)
|
|
|
|
|
|
|
+ Args:
|
|
|
|
|
+ dataset (ADNIDataset): The dataset to divide.
|
|
|
|
|
+ ptids (List[Tuple[int, str]]): A list of tuples containing image file IDs and their corresponding patient IDs.
|
|
|
|
|
+ ratios (Tuple[float, float, float]): The ratios for training, validation, and test sets.
|
|
|
|
|
+ seed (int): The random seed for reproducibility.
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ List[data.Subset[ADNIDataset]]: A list of subsets for training, validation, and test sets.
|
|
|
|
|
+
|
|
|
|
|
+ Notes:
|
|
|
|
|
+ This split is grouped by PTID, so all images from the same patient are assigned
|
|
|
|
|
+ to exactly one partition to avoid patient-level leakage across train/val/test.
|
|
|
|
|
+ """
|
|
|
|
|
+ if not math.isclose(sum(ratios), 1.0, rel_tol=1e-9, abs_tol=1e-9):
|
|
|
|
|
+ raise ValueError(f"Ratios must sum to 1.0, got {ratios} (sum={sum(ratios)}).")
|
|
|
|
|
+
|
|
|
|
|
+ patient_to_indices = _patient_index(dataset, ptids)
|
|
|
|
|
|
|
|
shuffled_patients = list(patient_to_indices.keys())
|
|
shuffled_patients = list(patient_to_indices.keys())
|
|
|
random.Random(seed).shuffle(shuffled_patients)
|
|
random.Random(seed).shuffle(shuffled_patients)
|
|
@@ -402,3 +409,83 @@ def divide_dataset_by_patient_id(
|
|
|
Subset(dataset, val_indices),
|
|
Subset(dataset, val_indices),
|
|
|
Subset(dataset, test_indices),
|
|
Subset(dataset, test_indices),
|
|
|
]
|
|
]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def kfold_split_by_patient_id(
|
|
|
|
|
+ dataset: ADNIDataset,
|
|
|
|
|
+ ptids: List[Tuple[int, str]],
|
|
|
|
|
+ k_folds: int,
|
|
|
|
|
+ fold: int,
|
|
|
|
|
+ seed: int,
|
|
|
|
|
+ val_fraction: float = 0.15,
|
|
|
|
|
+) -> List[data.Subset[ADNIDataset]]:
|
|
|
|
|
+ """Patient-grouped k-fold partition for a single fold.
|
|
|
|
|
+
|
|
|
|
|
+ Patients are shuffled once (deterministically from ``seed``) and dealt
|
|
|
|
|
+ round-robin into ``k_folds`` folds, so every patient is in exactly one fold
|
|
|
|
|
+ regardless of which fold is requested. For the requested ``fold``:
|
|
|
|
|
+ - test = that fold's patients (the held-out slice),
|
|
|
|
|
+ - the remaining patients are split into val/train by ``val_fraction``.
|
|
|
|
|
+ All splits are patient-disjoint (no leakage), and across all folds every
|
|
|
|
|
+ sample is a test point exactly once.
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ dataset: The dataset to partition.
|
|
|
|
|
+ ptids: (Image Data ID, PTID) pairs covering the dataset.
|
|
|
|
|
+ k_folds: Number of folds (>= 2).
|
|
|
|
|
+ fold: Which fold to hold out as test (0 <= fold < k_folds).
|
|
|
|
|
+ seed: Seed for the (shared across folds) patient shuffle.
|
|
|
|
|
+ val_fraction: Fraction of the NON-test patients used for validation.
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ ``[train, val, test]`` subsets.
|
|
|
|
|
+ """
|
|
|
|
|
+ if k_folds < 2:
|
|
|
|
|
+ raise ValueError(f"k_folds must be >= 2, got {k_folds}.")
|
|
|
|
|
+ if not 0 <= fold < k_folds:
|
|
|
|
|
+ raise ValueError(f"fold must be in [0, {k_folds}), got {fold}.")
|
|
|
|
|
+ if not 0.0 <= val_fraction < 1.0:
|
|
|
|
|
+ raise ValueError(f"val_fraction must be in [0.0, 1.0), got {val_fraction}.")
|
|
|
|
|
+
|
|
|
|
|
+ patient_to_indices = _patient_index(dataset, ptids)
|
|
|
|
|
+ patients = list(patient_to_indices.keys())
|
|
|
|
|
+ if len(patients) < k_folds:
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ f"Cannot make {k_folds} folds from {len(patients)} patients."
|
|
|
|
|
+ )
|
|
|
|
|
+ # Deterministic shuffle shared across all folds, then round-robin deal so
|
|
|
|
|
+ # fold sizes are balanced and the same partition is reproduced for every fold.
|
|
|
|
|
+ random.Random(seed).shuffle(patients)
|
|
|
|
|
+ fold_patients = [patients[i::k_folds] for i in range(k_folds)]
|
|
|
|
|
+
|
|
|
|
|
+ test_patients = fold_patients[fold]
|
|
|
|
|
+ rest = [p for f in range(k_folds) if f != fold for p in fold_patients[f]]
|
|
|
|
|
+ n_val = int(round(len(rest) * val_fraction))
|
|
|
|
|
+ val_patients = rest[:n_val]
|
|
|
|
|
+ train_patients = rest[n_val:]
|
|
|
|
|
+
|
|
|
|
|
+ def _indices(group: List[str]) -> List[int]:
|
|
|
|
|
+ return [idx for patient in group for idx in patient_to_indices[patient]]
|
|
|
|
|
+
|
|
|
|
|
+ train_indices = _indices(train_patients)
|
|
|
|
|
+ val_indices = _indices(val_patients)
|
|
|
|
|
+ test_indices = _indices(test_patients)
|
|
|
|
|
+
|
|
|
|
|
+ # Safety: patient-disjoint and full, exact coverage of the dataset.
|
|
|
|
|
+ train_set, val_set, test_set = (
|
|
|
|
|
+ set(train_indices),
|
|
|
|
|
+ set(val_indices),
|
|
|
|
|
+ set(test_indices),
|
|
|
|
|
+ )
|
|
|
|
|
+ if train_set & val_set or train_set & test_set or val_set & test_set:
|
|
|
|
|
+ raise ValueError("Sample index overlap detected across k-fold splits.")
|
|
|
|
|
+ if train_set | val_set | test_set != set(range(len(dataset))):
|
|
|
|
|
+ raise ValueError(
|
|
|
|
|
+ "k-fold coverage check failed: not all samples assigned exactly once."
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ Subset(dataset, train_indices),
|
|
|
|
|
+ Subset(dataset, val_indices),
|
|
|
|
|
+ Subset(dataset, test_indices),
|
|
|
|
|
+ ]
|