Repository: mlcommons/GaNDLF

Issue:
[FEATURE] Add the ability to split CSVs for training/validation/testing as a separate script
### Is your feature request related to a problem? Please describe.
Currently, the only way for a user to split a data CSV is to run GaNDLF and store the CSV outputs from [`training_manager`](https://github.com/mlcommons/GaNDLF/blob/master/GANDLF/training_manager.py#L244), which is very cumbersome.

### Describe the solution you'd like
It would be good to have this as an independent script so that users can just invoke that and not have to bother with running the full `training_manager`.

### Describe alternatives you've considered
N.A.

### Additional context
N.A.



Discussion:
Please assign to me.

Resolving patch:
diff --git a/GANDLF/cli/__init__.py b/GANDLF/cli/__init__.py
--- a/GANDLF/cli/__init__.py
+++ b/GANDLF/cli/__init__.py
@@ -6,6 +6,7 @@
 from .recover_config import recover_config
 from .post_training_model_optimization import post_training_model_optimization
 from .generate_metrics import generate_metrics_dict
+from .data_split_saver import split_data_and_save_csvs
 
 from datetime import date
 
diff --git a/GANDLF/cli/data_split_saver.py b/GANDLF/cli/data_split_saver.py
new file mode 100644
--- /dev/null
+++ b/GANDLF/cli/data_split_saver.py
@@ -0,0 +1,49 @@
+from typing import Union
+import os
+
+import pandas as pd
+from GANDLF.utils import get_dataframe, split_data
+
+
+def split_data_and_save_csvs(
+    input_data: Union[pd.DataFrame, str], output_dir: str, parameters: dict
+) -> None:
+    """
+    Split the data into training, validation, and testing sets and save them as csvs in the output directory
+
+    Args:
+        input_data (Union[pd.Dataframe, str]): The input data to be split and saved.
+        output_dir (str): The output directory to save the split data.
+        parameters (dict): The parameters dictionary.
+    """
+
+    full_data = get_dataframe(input_data)
+
+    dataframe_split = split_data(full_data, parameters)
+
+    for (
+        testing_and_valid_indeces,
+        trainingData,
+        validationData,
+        testingData,
+    ) in dataframe_split:
+        # training and validation dataframes use the same index, since they are based on the validation split
+        training_data_path = os.path.join(
+            output_dir, f"training_{testing_and_valid_indeces[1]}.csv"
+        )
+        validation_data_path = os.path.join(
+            output_dir, f"validation_{testing_and_valid_indeces[1]}.csv"
+        )
+        # testing dataframes use the first index
+        testing_data_path = os.path.join(
+            output_dir, f"testing_{testing_and_valid_indeces[0]}.csv"
+        )
+
+        for data, path in zip(
+            [trainingData, validationData, testingData],
+            [training_data_path, validation_data_path, testing_data_path],
+        ):
+            # check if the data is not None and the path does not exist
+            if not os.path.exists(path):
+                if data is not None:
+                    data.to_csv(path, index=False)


Test patch:
diff --git a/testing/test_full.py b/testing/test_full.py
--- a/testing/test_full.py
+++ b/testing/test_full.py
@@ -32,6 +32,7 @@
     recover_config,
     post_training_model_optimization,
     generate_metrics_dict,
+    split_data_and_save_csvs,
 )
 from GANDLF.schedulers import global_schedulers_dict
 from GANDLF.optimizers import global_optimizer_dict
@@ -890,6 +891,7 @@ def test_train_inference_classification_with_logits_single_fold_rad_3d(device):
         testingDir + "/config_classification.yaml", version_check_flag=False
     )
     training_data, parameters["headers"] = parseTrainingCSV(temp_infer_csv)
+    parameters["output_dir"] = outputDir  # this is in inference mode
     parameters["modality"] = "rad"
     parameters["patch_size"] = patch_size["3D"]
     parameters["model"]["dimension"] = 3
@@ -3112,3 +3114,38 @@ def test_generic_deploy_metrics_docker():
     sanitize_outputDir()
 
     print("passed")
+
+
+def test_generic_data_split():
+    print("51: Starting test for splitting and saving CSVs")
+    # read and initialize parameters for specific data dimension
+    parameters = ConfigManager(
+        testingDir + "/config_classification.yaml", version_check_flag=False
+    )
+    parameters["nested_training"] = {"testing": 5, "validation": 5, "stratified": True}
+    # read and parse csv
+    training_data, parameters["headers"] = parseTrainingCSV(
+        inputDir + "/train_3d_rad_classification.csv"
+    )
+    parameters["model"]["num_channels"] = len(parameters["headers"]["channelHeaders"])
+    parameters = populate_header_in_parameters(parameters, parameters["headers"])
+    # duplicate the data to test stratified sampling
+    training_data_duplicate = training_data._append(training_data)
+    for _ in range(1):
+        training_data_duplicate = training_data_duplicate._append(
+            training_data_duplicate
+        )
+    training_data_duplicate.reset_index(drop=True, inplace=True)
+    # ensure subjects are not duplicated
+    training_data_duplicate["SubjectID"] = training_data_duplicate.index
+
+    sanitize_outputDir()
+
+    split_data_and_save_csvs(training_data_duplicate, outputDir, parameters)
+
+    files_in_outputDir = os.listdir(outputDir)
+    assert len(files_in_outputDir) == 15, "CSVs were not split correctly"
+
+    sanitize_outputDir()
+
+    print("passed")