This repository serves as a reference for the preprocessing, training, and evaluation protocols, with scripts enabling fair and reproducible benchmarking.
Here we evaluate patient stratification tasks using bulk transcriptomic data (RNA-seq and microarray), classical machine-learning models, neural networks, graph models, dimensionality-reduction methods, and synthetic patient generation.
Clinical transcriptomic datasets are increasingly used to train machine learning models, yet they are often treated uniformly despite substantial biological differences. Focusing on cancer subtype stratification, we introduce the concept of the complexity spiral: the tendency to prioritize increasingly sophisticated architectures over data-centric rigor. Using seven bulk transcriptomic cohorts, we benchmarked preprocessing and modeling strategies across the full pipeline, including normalization, batch correction, dimensionality reduction, and synthetic patient generation. We found that inadequate preprocessing and evaluation choices can artificially inflate performance, while greater model complexity rarely compensates for poor data quality. Our results highlight that it is essential to attend to the nature and limitations of the data, focusing on patient heterogeneity, label type, and high feature-to-sample ratios. In particular, we introduce a novel classification of biological label categories (molecular, histological, or clinical) that strongly influences task difficulty and achievable performance, with molecular labels consistently yielding higher accuracy than clinical outcomes. Further, deep learning models tend to overfit under low-sample regimes, while simpler tree-based methods, such as Random Forest and XGBoost, achieve more stable generalization across splits and provide robust baselines, often matching or outperforming more advanced approaches. Finally, we propose a decalogue of good practices and a ready-to-use code framework that, if adopted, will support researchers in developing translationally relevant and clinically reliable models.
The benchmark covers seven biological cohorts. Some cohorts expose several prediction tasks or batch-correction variants, so run.py accepts the following dataset identifiers:
| Cohort | Dataset identifier(s) | Data type |
|---|---|---|
| Breast cancer metastasis | BreastCancer_M |
Microarray |
| Breast cancer | BreastCancer_S, BreastCancer_S_binarized, BreastCancer_S_Histological, BreastCancer_S_Clinical |
RNA-seq |
| Multiple myeloma | MultipleMyeloma |
RNA-seq |
| Prostate Gleason | Prostate_Gleason |
RNA-seq |
| Lower-grade glioma | LowerGradeGlioma, LowerGradeGlioma_Histological, LowerGradeGlioma_Clinical |
RNA-seq |
| Lung histology | Lung_Histological, Lung_Histological_No_ComBat, Lung_Histological_limma, Lung_Histological_POIBM_rho1, Lung_Histological_POIBM_rho05 |
RNA-seq |
| Kidney subtype | Kidney_Subtype, Kidney_Subtype_No_ComBat, Kidney_Subtype_limma, Kidney_Subtype_POIBM_rho1, Kidney_Subtype_POIBM_rho05 |
RNA-seq |
Raw inputs are expected under data/raw/<data_type>/<dataset>/. Derived normalized matrices and encoded labels are cached under data/preprocessed/ when run.py first needs them. Derived breast and glioma tasks reuse the raw files of BreastCancer_S and LowerGradeGlioma, respectively.
From the repository root, build the image and mount the repository at the image working directory:
docker build -t phenotypic-prediction:latest Dockers/Pytorch_cuda12_R4_4/
docker run --gpus all --rm -it -v "$(pwd):/wdir" phenotypic-prediction:latestThe image is based on PyTorch 2.5.1 with CUDA 12.4 and cuDNN 9. It also installs the Python, R, Bioconductor, PyTorch Geometric, and POIBM dependencies used by the project. NVIDIA Container Toolkit and a compatible GPU driver are required for --gpus all. Synthetic generation currently requests CUDA explicitly.
There is currently no separate requirements file. Use Dockers/Pytorch_cuda12_R4_4/Dockerfile as the authoritative list of pinned Python packages, R packages, and system dependencies.
Here are some example commands to get started:
# Run a Random Forest experiment on breast cancer data
python3 -u run.py -d BreastCancer_S -m Random_Forest -n zscore -p None -s 42 -Nf 5
# Run with synthetic data generation (VAE)
python3 -u run.py -d BreastCancer_S -m MLP -n zscore -p VAE -Mf 1.0 -s 42 -Nf 5
# Run transfer learning
python3 -u run_transfer_learning.py -d BreastCancer_S -n zscore -p None -s 42 -Nf 5data/raw/: Raw datasets before preprocessingdata/preprocessed/: Preprocessed datasets ready for model trainingcode/models.py: Model implementations (sklearn and PyTorch Lightning)code/utils.py: Training and grid search utility functionscode/preprocessing/: R scripts for dataset download and preprocessingresults/: Grid-search results, trained models, configurations, and evaluation metricslogs/: Optional location for redirected console output (the scripts do not create log files automatically)Dockers/: Reproducible Docker environment
python3 -u run.py \
-d <dataset> \
-m <model> \
[-n <normalization>] \
[-p <preprocessing>] \
[-s <seed>] \
[-Nf <number_folds>] \
[-Mf <multiplicative_factor>] \
[-Vs] \
[-gs <grid_search_csv>] \
[-Pf <patient_fraction>]Required arguments:
-d,--dataset: one of the exact identifiers in the Datasets table.-m,--model:Logistic_Regression,Logistic_Regression_Ridge,Logistic_Regression_ElasticNet,Random_Forest,Random_Forest_balanced,SVM,XGBoost,MLP,MLP_mixup,SPARSE_NN,VAE_CL,SPARSE_VAE_CL,GNN,GAT,Graph_Transformer,ChebNet, orSpectral_NN.
Optional arguments:
-n,--normalization:none,zscore, orminmax(default:zscore).-p,--preprocess: one or more ordered preprocessing steps separated by_(default:None). Valid steps areNone,VAE,WGAN-GP,NetActivity,DEG,SPECTRAL-SM-<threshold>-<k>,SPECTRAL-LM-<threshold>-<k>,MaxVar-<n>, andRandomGenes-<n>. Examples:MaxVar-500,VAE_NetActivity, andWGAN-GP_DEG.-s,--seed: random seed (default:42).-Nf,--N_folds: number of train/validation folds used for final evaluation (default:10). A fixed stratified test set is held out before these folds are generated.-Mf,--multiplicative_factor: number of synthetic samples relative to the original sample count; relevant toVAEandWGAN-GP(default:1.0).-Vs,--validate_synthetic: evaluate generated synthetic data.-gs,--gridsearch_result_file: grid-search CSV to reuse. If the path does not exist, the grid search is run and written there (default: an automatically generated path underresults/).-Pf,--patient_fraction: fraction of the train/validation patient pool to retain, in(0, 1](default:1.0). The test set is unchanged.
Argument values are case-sensitive. Run python3 run.py --help for the CLI summary.
This script trains an MLP on PAN-CANCER data (excluding patients from the target dataset) and applies transfer learning to solve the target classification problem.
python3 -u run_transfer_learning.py -d <dataset> -n <normalization> -p <preprocessing> -s <seed> -Nf <number_folds>Supported datasets are BreastCancer_M, BreastCancer_S, BreastCancer_S_binarized, MultipleMyeloma, Prostate_Gleason, LowerGradeGlioma, Lung_Histological, Lung_Histological_No_ComBat, Kidney_Subtype, and Kidney_Subtype_No_ComBat. The normalization, seed, and fold arguments have the same defaults as run.py; preprocessing is limited to None, NetActivity, or DEG. The PAN-CANCER raw files must also be available (see Data preprocessing).
After running an experiment, the results folder will have the following structure:
results/
βββ rnaseq/ # Data type (rnaseq or microarray)
β βββ BreastCancer_S/ # Dataset name
β βββ SVM/ # Model name
β β βββ GridSearchs/ # Grid search results
β β β βββ n_zscore_p_None_s_42_N_10.csv
β β βββ Best_models/ # Trained models
β β βββ n_zscore_p_None_s_42_N_10/
β β βββ scores.csv # Evaluation metrics (see below)
β β βββ model_weights.pkl # scikit-learn estimator
β β βββ model_config.json # Hyperparameter configuration
β βββ MLP/
β β βββ GridSearchs/
β β βββ Best_models/
βββ microarray/
βββ BreastCancer_M/
βββ ...
Neural and graph models use model_weights.pth instead. Synthetic-data runs add _Mf_<factor> to generated names, and subsampling runs add _Pf_<fraction> when the fraction is not 1.0.
Each row corresponds to one train/validation fold; there is no explicit fold column. Binary experiments report time(min) plus roc_auc, accuracy, aupr, balanced_f1, and balanced_acc for each of the train, val, and test prefixes. Multiclass experiments report the same split prefixes with roc_auc_ovr, roc_auc_ovo, accuracy, aupr_weighted, balanced_f1, and balanced_acc.
The scripts under code/preprocessing/ are executed independently of run.py. They should be run from the repository root so that their relative output paths resolve correctly.
cd /path/to/Phenotypic_prediction
Rscript code/preprocessing/<script_name>.Rrun.py expects the processed inputs to exist under data/raw/. In general, the workflow is:
- Run the dataset-specific preprocessing script from the repository root.
- Verify that the expected files were created under
data/raw/. - Run
python3 -u run.py ...orpython3 -u run_transfer_learning.py ....
BreastCancer_S,BreastCancer_S_binarized,BreastCancer_S_Histological, andBreastCancer_S_Clinical: RunRscript code/preprocessing/breast_subtypes_tcga_download.RThis createsTCGA-BRCA/in the current working directory with BRCA counts, TPMs, and metadata files. Forrun.py, placeTCGA-BRCA_tpm_unstrand.csvandTCGA-BRCA_col_data.csvunderdata/raw/rnaseq/BreastCancer_S/. All four tasks use the same raw files.LowerGradeGlioma,LowerGradeGlioma_Histological, andLowerGradeGlioma_Clinical: RunRscript code/preprocessing/lgg_download.RThis createsTCGA-LGG/in the current working directory with counts, TPMs, and metadata files. Forrun.py, placeTCGA-LGG_tpm_unstrand.csvandTCGA-LGG_col_data.csvunderdata/raw/rnaseq/LowerGradeGlioma/. All three tasks use these raw files.Lung_HistologicalandLung_Histological_No_ComBat: RunRscript code/preprocessing/lung_download.RBy default the script setsapply_combat <- FALSEand writes the no-ComBat dataset todata/raw/rnaseq/Lung_Histological_No_ComBat/. To generate the ComBat-corrected version, edit the script and setapply_combat <- TRUE; it will then write todata/raw/rnaseq/Lung_Histological/.Kidney_SubtypeandKidney_Subtype_No_ComBat: RunRscript code/preprocessing/kidney_donwload.RBy default the script setsapply_combat <- FALSEand writes the no-ComBat dataset todata/raw/rnaseq/Kidney_Subtype_No_ComBat/. To generate the ComBat-corrected version, edit the script and setapply_combat <- TRUE; it will then write todata/raw/rnaseq/Kidney_Subtype/.- limma variants:
Run
Rscript code/preprocessing/lung_download_limma.RorRscript code/preprocessing/kidney_donwload_limma.RforLung_Histological_limmaorKidney_Subtype_limma, respectively. - POIBM variants:
Run
Rscript code/preprocessing/lung_download_POIBM.RorRscript code/preprocessing/kidney_donwload_POIBM.R. Each script contains arho_05switch that selects therho05orrho1output directory. MultipleMyeloma: RunRscript code/preprocessing/multiplemyeloma_preprocess.RThis script expects the original CoMMpass files to already be available in./MultipleMyeloma_RAW/relative to the working directory. It generates filtered count and metadata files inside./MultipleMyeloma_RAW/.- Transfer learning PAN-CANCER reference:
Run
Rscript code/preprocessing/pan_cancer_download.RThis writes the PAN-CANCER reference dataset todata/raw/rnaseq/PAN-CANCER/.
- The TCGA multi-project scripts (
lung_download*.R,kidney_donwload*.R, andpan_cancer_download.R) write directly intodata/raw/...and should be run from the repository root. multiplemyeloma_preprocess.R,breast_subtypes_tcga_download.R, andlgg_download.Rwrite to relative folders created from the current working directory. For reproducibility, run them from the repository root and then move or integrate the generated files into the expecteddata/raw/layout if needed.- For
BreastCancer_SandLowerGradeGlioma, a minimal setup after running the R script is:
mkdir -p data/raw/rnaseq/BreastCancer_S data/raw/rnaseq/LowerGradeGlioma
cp TCGA-BRCA/TCGA-BRCA_tpm_unstrand.csv TCGA-BRCA/TCGA-BRCA_col_data.csv data/raw/rnaseq/BreastCancer_S/
cp TCGA-LGG/TCGA-LGG_tpm_unstrand.csv TCGA-LGG/TCGA-LGG_col_data.csv data/raw/rnaseq/LowerGradeGlioma/- After raw data is present,
run.pycreates derived files indata/preprocessed/automatically when they are missing.
Add a function in code/models.py:
def YourModel(**kwargs):
return YourSklearnModel(random_state=42, **kwargs)Then add configuration in run.py:
'YourModel': {
'param_grid': {'param1': [1, 2], 'param2': [0.1, 0.5]},
'model_utils': {
'grid_search_type': 'gridsearch_sklearn',
'N_folds_to_use': 5,
'train_type': 'train_sklearn'
}
}Create a class in code/models.py inheriting from pl.LightningModule:
import pytorch_lightning as pl
class YourModel(pl.LightningModule):
def __init__(self, input_dim, output_dim, **kwargs):
super().__init__()
# Define layers
self.model = nn.Sequential(...)
self.loss_fn = nn.CrossEntropyLoss()
def forward(self, x):
return self.model(x)
def configure_optimizers(self):
return torch.optim.Adam(self.parameters(), lr=0.001)
def training_step(self, batch, batch_idx):
x, y = batch
y_hat = self(x)
loss = self.loss_fn(y_hat, y)
return loss
def validation_step(self, batch, batch_idx):
x, y = batch
y_hat = self(x)
loss = self.loss_fn(y_hat, y)
self.log('val_loss', loss)
def test_step(self, batch, batch_idx):
x, y = batch
y_hat = self(x)
# Compute and log test metrics
loss = self.loss_fn(y_hat, y)
self.log('test_loss', loss)Then add configuration in run.py:
'YourModel': {
'param_grid': {'learning_rate': [0.001, 0.01], 'hidden_dim': [64, 128]},
'model_utils': {
'grid_search_type': 'gridsearch_nn',
'N_folds_to_use': 5,
'train_type': 'train_nn'
}
}- Grid search function: Must match
grid_search_typename incode/utils.py - Training function: Must match
train_typename and savescores.csvwith metrics from the "Output Example" section - N_folds_to_use: Typically smaller than final cross-validation folds (5-10) to speed up grid search
See code/models.py for implementation examples of each model type.
To add a new dataset, complete these three steps: place raw files in the expected folder, register the dataset in run.py, and add dataset-specific preprocessing in code/data_utils.py.
Create:
data/raw/<data_type>/<DatasetName>/
Where:
<data_type>ismicroarrayorrnaseq<DatasetName>is the exact dataset name you will pass with-d/--datasetinrun.py
At minimum, include:
- Expression matrix file (genes in rows, samples in columns)
- Metadata/labels file
In the available_datasets dictionary, add an entry like:
'YourDataset': {
'data_type': 'rnaseq', # or 'microarray'
'files_names': {
'data': 'your_expression_file.csv',
'labels': 'your_labels_file.csv'
},
'normalizations': ['none', 'zscore', 'minmax']
},Inside preprocess_dataset(...), add a new branch for your dataset:
elif dataset == 'YourDataset':
data_matrix = pd.read_csv(os.path.join(raw_path, data_file_name), index_col=0)
metadata = pd.read_csv(os.path.join(raw_path, labels_file_name), index_col=0)
# Build labels table
df_labels = metadata[['barcode', 'class_name']].copy()
label_map = {'ClassA': 0, 'ClassB': 1}
df_labels.to_csv(os.path.join(preprocessed_folder, 'Labels_df.tsv'), index=False, sep='\t')
df_labels.set_index('barcode', inplace=True)
labels_data = df_labels['class_name'].map(label_map).rename('label').to_frame()
with open(os.path.join(preprocessed_folder, 'name2label.json'), 'w') as f:
json.dump(label_map, f)
# Keep only labeled samples and remove all-zero genes
raw_data_matrix = data_matrix[df_labels.index.tolist()]
raw_data_matrix = raw_data_matrix.loc[~(raw_data_matrix == 0).all(axis=1)]
df_zscore = normalize_dataframe(raw_data_matrix, normalization)Expected outputs created automatically under data/preprocessed/<data_type>/<DatasetName>/<normalization>/:
normalized_gene_expression.csvlabels.csv(index = sample IDs, one integer column namedlabel)
- Automatic download currently exists only for
BreastCancer_M. If needed, also extenddownload_dataset(...)incode/data_utils.py. - If you plan to use
-p DEGwith an RNA-seq dataset, ensure the expected raw count-format file exists forapply_DE_analysis(...).
BreastCancer_Mis the only dataset downloaded automatically byrun.py.- The breast, glioma, lung, kidney, and PAN-CANCER inputs are prepared with the R scripts listed above.
MultipleMyelomarequires the original CoMMpass files inMultipleMyeloma_RAW/before its preprocessing script is run.- Once raw inputs exist, normalized matrices and labels are generated and cached under
data/preprocessed/.
- Out of memory: Reduce the relevant batch-size or model-grid values in the source configuration.
- Device not found: Check the NVIDIA driver and container runtime with
nvidia-smianddocker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi. - CPU-only execution: Neural classifiers select CPU automatically when CUDA is unavailable, but the synthetic
VAEandWGAN-GPpath currently passesdevice='cuda'explicitly.
- Import errors: Verify all packages installed:
python3 -c "import torch, pytorch_lightning, sklearn" - Missing package: Compare the local environment with the pinned packages in the Dockerfile.
- R package installation fails: Rebuild Docker image to ensure all Bioconductor packages install
- Memory issues: Large datasets may require 16GB+ RAM for preprocessing
- Model divergence: Try lower learning rates (0.0001) or different activation functions
- Grid search too slow: Reduce
N_folds_to_usein model config - Results not saved: Verify write permissions in
results/folder