Skip to content

Latest commit

Β 

History

11 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Rethinking Machine Learning in Clinical Transcriptomics: A Comprehensive Benchmarking Study

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.

Abstract

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.

Datasets

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.

Installation & Environment Setup

Option 1: Docker (recommended)

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:latest

The 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.

Option 2: Local Installation

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.

Quick Start

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 5

Project Structure

  • data/raw/: Raw datasets before preprocessing
  • data/preprocessed/: Preprocessed datasets ready for model training
  • code/models.py: Model implementations (sklearn and PyTorch Lightning)
  • code/utils.py: Training and grid search utility functions
  • code/preprocessing/: R scripts for dataset download and preprocessing
  • results/: Grid-search results, trained models, configurations, and evaluation metrics
  • logs/: Optional location for redirected console output (the scripts do not create log files automatically)
  • Dockers/: Reproducible Docker environment

How to run an experiment

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, or Spectral_NN.

Optional arguments:

  • -n, --normalization: none, zscore, or minmax (default: zscore).
  • -p, --preprocess: one or more ordered preprocessing steps separated by _ (default: None). Valid steps are None, VAE, WGAN-GP, NetActivity, DEG, SPECTRAL-SM-<threshold>-<k>, SPECTRAL-LM-<threshold>-<k>, MaxVar-<n>, and RandomGenes-<n>. Examples: MaxVar-500, VAE_NetActivity, and WGAN-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 to VAE and WGAN-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 under results/).
  • -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.

How to run transfer learning

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).

Output Example

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.

scores.csv columns

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.

Data preprocessing

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>.R

run.py expects the processed inputs to exist under data/raw/. In general, the workflow is:

  1. Run the dataset-specific preprocessing script from the repository root.
  2. Verify that the expected files were created under data/raw/.
  3. Run python3 -u run.py ... or python3 -u run_transfer_learning.py ....

Dataset-specific preprocessing scripts

  • BreastCancer_S, BreastCancer_S_binarized, BreastCancer_S_Histological, and BreastCancer_S_Clinical: Run Rscript code/preprocessing/breast_subtypes_tcga_download.R This creates TCGA-BRCA/ in the current working directory with BRCA counts, TPMs, and metadata files. For run.py, place TCGA-BRCA_tpm_unstrand.csv and TCGA-BRCA_col_data.csv under data/raw/rnaseq/BreastCancer_S/. All four tasks use the same raw files.
  • LowerGradeGlioma, LowerGradeGlioma_Histological, and LowerGradeGlioma_Clinical: Run Rscript code/preprocessing/lgg_download.R This creates TCGA-LGG/ in the current working directory with counts, TPMs, and metadata files. For run.py, place TCGA-LGG_tpm_unstrand.csv and TCGA-LGG_col_data.csv under data/raw/rnaseq/LowerGradeGlioma/. All three tasks use these raw files.
  • Lung_Histological and Lung_Histological_No_ComBat: Run Rscript code/preprocessing/lung_download.R By default the script sets apply_combat <- FALSE and writes the no-ComBat dataset to data/raw/rnaseq/Lung_Histological_No_ComBat/. To generate the ComBat-corrected version, edit the script and set apply_combat <- TRUE; it will then write to data/raw/rnaseq/Lung_Histological/.
  • Kidney_Subtype and Kidney_Subtype_No_ComBat: Run Rscript code/preprocessing/kidney_donwload.R By default the script sets apply_combat <- FALSE and writes the no-ComBat dataset to data/raw/rnaseq/Kidney_Subtype_No_ComBat/. To generate the ComBat-corrected version, edit the script and set apply_combat <- TRUE; it will then write to data/raw/rnaseq/Kidney_Subtype/.
  • limma variants: Run Rscript code/preprocessing/lung_download_limma.R or Rscript code/preprocessing/kidney_donwload_limma.R for Lung_Histological_limma or Kidney_Subtype_limma, respectively.
  • POIBM variants: Run Rscript code/preprocessing/lung_download_POIBM.R or Rscript code/preprocessing/kidney_donwload_POIBM.R. Each script contains a rho_05 switch that selects the rho05 or rho1 output directory.
  • MultipleMyeloma: Run Rscript code/preprocessing/multiplemyeloma_preprocess.R This 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.R This writes the PAN-CANCER reference dataset to data/raw/rnaseq/PAN-CANCER/.

Notes on outputs and working directory

  • The TCGA multi-project scripts (lung_download*.R, kidney_donwload*.R, and pan_cancer_download.R) write directly into data/raw/... and should be run from the repository root.
  • multiplemyeloma_preprocess.R, breast_subtypes_tcga_download.R, and lgg_download.R write 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 expected data/raw/ layout if needed.
  • For BreastCancer_S and LowerGradeGlioma, 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.py creates derived files in data/preprocessed/ automatically when they are missing.

Contributing: Adding New Models

For Sklearn-based models (Logistic_Regression, Random_Forest, SVM, XGBoost)

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'
    }
}

For PyTorch Lightning models (MLP, VAE_CL, GNN, Spectral_NN, SPARSE_NN)

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'
    }
}

Key Implementation Details

  • Grid search function: Must match grid_search_type name in code/utils.py
  • Training function: Must match train_type name and save scores.csv with 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.

Contributing: Adding New Datasets

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.

1) Place raw files under data/raw/

Create:

data/raw/<data_type>/<DatasetName>/

Where:

  • <data_type> is microarray or rnaseq
  • <DatasetName> is the exact dataset name you will pass with -d/--dataset in run.py

At minimum, include:

  • Expression matrix file (genes in rows, samples in columns)
  • Metadata/labels file

2) Register the dataset in run.py

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']
},

3) Add preprocessing logic in code/data_utils.py

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.csv
  • labels.csv (index = sample IDs, one integer column named label)

Notes

  • Automatic download currently exists only for BreastCancer_M. If needed, also extend download_dataset(...) in code/data_utils.py.
  • If you plan to use -p DEG with an RNA-seq dataset, ensure the expected raw count-format file exists for apply_DE_analysis(...).

Data availability summary

  • BreastCancer_M is the only dataset downloaded automatically by run.py.
  • The breast, glioma, lung, kidney, and PAN-CANCER inputs are prepared with the R scripts listed above.
  • MultipleMyeloma requires the original CoMMpass files in MultipleMyeloma_RAW/ before its preprocessing script is run.
  • Once raw inputs exist, normalized matrices and labels are generated and cached under data/preprocessed/.

Troubleshooting

CUDA/GPU Issues

  • 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-smi and docker 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 VAE and WGAN-GP path currently passes device='cuda' explicitly.

Python/Package Issues

  • 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/Data Preprocessing Issues

  • R package installation fails: Rebuild Docker image to ensure all Bioconductor packages install
  • Memory issues: Large datasets may require 16GB+ RAM for preprocessing

Training Issues

  • Model divergence: Try lower learning rates (0.0001) or different activation functions
  • Grid search too slow: Reduce N_folds_to_use in model config
  • Results not saved: Verify write permissions in results/ folder

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages