ACDC_CDS_NICMS / split_datasets.py
TaipingQu's picture
Upload split_datasets.py
9f46084 verified
Raw History Blame Contribute Delete
6.47 kB
#!/usr/bin/env python3
import argparse
import csv
import os
import random
import sys
def parse_args():
parser = argparse.ArgumentParser(description='Stratified train/valid/test split with 7:1:2 ratio')
parser.add_argument(
'--input_csv',
default='/home/qutaiping/nas/zhaocan/heart_diagnosis/diag_second_data.csv',
)
parser.add_argument(
'--output_dir',
default='/home/qutaiping/nas/zhaocan/heart_diagnosis/diag_data_split/',
)
parser.add_argument('--seed', type=int, default=42)
return parser.parse_args()
def load_csv(csv_path):
"""Load CSV file and return header and rows."""
with open(csv_path, 'r', newline='', encoding='utf-8-sig') as f:
reader = csv.reader(f)
header = next(reader)
rows = list(reader)
return header, rows
def calculate_split_sizes(total, ratios=(0.7, 0.1, 0.2)):
"""
Calculate integer sample sizes according to the given ratios.
Uses the largest remainder method to ensure that:
train + valid + test = total
"""
raw_sizes = [total * ratio for ratio in ratios]
sizes = [int(size) for size in raw_sizes]
remaining = total - sum(sizes)
remainders = [(raw_sizes[i] - sizes[i], i) for i in range(len(ratios))]
remainders.sort(reverse=True)
for _, index in remainders[:remaining]:
sizes[index] += 1
return sizes
def allocate_by_class(class_counts, target_size, total_size):
"""
Allocate target_size samples among classes according to their original class proportions.
Uses the largest remainder method for integer allocation.
"""
allocations = {}
remainders = []
for cls, count in class_counts.items():
raw = count * target_size / total_size
base = int(raw)
allocations[cls] = base
remainders.append((raw - base, cls))
current_total = sum(allocations.values())
remaining = target_size - current_total
# Classes with larger fractional parts receive the remaining samples first.
remainders.sort(key=lambda x: (-x[0], x[1]))
for _, cls in remainders[:remaining]:
allocations[cls] += 1
return allocations
def stratified_split(rows, header, seed):
"""
Perform stratified 7:1:2 train/valid/test split.
"""
class_idx = header.index('class')
# Group samples by class.
by_class = {}
for row in rows:
cls = row[class_idx].strip()
by_class.setdefault(cls, []).append(row)
total_size = len(rows)
if total_size == 0:
raise ValueError('No samples available for splitting.')
# Calculate total sizes of train/valid/test.
train_size, valid_size, test_size = calculate_split_sizes(total_size)
print('\n========== Dataset Size ==========')
print(f'Total: {total_size}')
print(
f'Train: {train_size} '
f'({train_size / total_size:.2%})'
)
print(
f'Valid: {valid_size} '
f'({valid_size / total_size:.2%})'
)
print(
f'Test : {test_size} '
f'({test_size / total_size:.2%})'
)
# Original number of samples in each class.
class_counts = {cls: len(items) for cls, items in by_class.items()}
train_counts = allocate_by_class(class_counts, train_size, total_size)
remaining_counts = {cls: (class_counts[cls] - train_counts[cls]) for cls in class_counts}
remaining_total = valid_size + test_size
valid_counts = allocate_by_class(remaining_counts, valid_size, remaining_total)
test_counts = {cls: (remaining_counts[cls] - valid_counts[cls]) for cls in class_counts}
print('\n========== Stratified Allocation ==========')
print(
f'{"Class":<15}'
f'{"Total":>10}'
f'{"Train":>10}'
f'{"Valid":>10}'
f'{"Test":>10}'
)
for cls in sorted(class_counts):
print(
f'{cls:<15}'
f'{class_counts[cls]:>10}'
f'{train_counts[cls]:>10}'
f'{valid_counts[cls]:>10}'
f'{test_counts[cls]:>10}'
)
# Randomly split samples within each class.
rng = random.Random(seed)
train_rows = []
valid_rows = []
test_rows = []
for cls, items in by_class.items():
items = items.copy()
# Shuffle samples within each class.
rng.shuffle(items)
n_train = train_counts[cls]
n_valid = valid_counts[cls]
train_rows.extend(items[:n_train])
valid_rows.extend(items[n_train:n_train + n_valid])
test_rows.extend(items[n_train + n_valid:])
# Shuffle each final dataset again so that different classes do not appear in blocks.
rng.shuffle(train_rows)
rng.shuffle(valid_rows)
rng.shuffle(test_rows)
return (train_rows, valid_rows, test_rows)
def write_csv(path, header, rows):
with open(path, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(header)
writer.writerows(rows)
def get_ids(rows, id_idx):
return [row[id_idx].strip() for row in rows]
def calculate_distribution(rows, class_idx):
"""Calculate class counts and class distribution."""
counts = {}
for row in rows:
cls = row[class_idx].strip()
counts[cls] = counts.get(cls, 0) + 1
total = len(rows)
distribution = {cls: (count / total if total else 0) for cls, count in counts.items()}
return counts, distribution
def main():
args = parse_args()
os.makedirs(args.output_dir, exist_ok=True)
header, rows = load_csv(args.input_csv)
class_idx = header.index('class')
class_counts, class_dist = (calculate_distribution(rows, class_idx))
print('\n========== Original Class Distribution ==========')
print(
f'{"Class":<15}'
f'{"Count":>10}'
f'{"Ratio":>12}'
)
for cls in sorted(class_counts):
print(
f'{cls:<15}'
f'{class_counts[cls]:>10}'
f'{class_dist[cls]:>11.2%}'
)
train_rows, valid_rows, test_rows = (stratified_split(rows, header, args.seed))
train_path = os.path.join(args.output_dir, 'stage2_train.csv')
valid_path = os.path.join(args.output_dir, 'stage2_valid.csv')
test_path = os.path.join(args.output_dir, 'stage2_test.csv')
write_csv(train_path, header, train_rows)
write_csv(valid_path, header, valid_rows)
write_csv(test_path, header, test_rows)
if __name__ == '__main__':
main()