Skip to contents

This tutorial runs the whole command-line workflow on the synthetic example that ships with rsemflow: six samples (three Control, three Drug, in two batches) and 200 genes. Every output shown below was produced by the commands on this page when the documentation was built.

The data are synthetic; the numbers illustrate the file formats, not biology.

0. The input

All outputs are plain TSV files. To look at them, this page uses a small helper that reads a table and shows its first rows:

show_table <- function(path, n = 6, cols = NULL) {
  x <- utils::read.delim(path, check.names = FALSE)
  if (!is.null(cols)) x <- x[, cols, drop = FALSE]
  utils::head(x, n)
}
.
├── data/
│   ├── C1.genes.results   C2.genes.results   C3.genes.results
│   └── D1.genes.results   D2.genes.results   D3.genes.results
└── metadata.tsv
show_table("metadata.tsv")
#>   sample_id condition batch age
#> 1        C1   Control     A  42
#> 2        C2   Control     B  51
#> 3        C3   Control     A  47
#> 4        D1      Drug     B  49
#> 5        D2      Drug     A  55
#> 6        D3      Drug     B  53

Each RSEM file is the standard gene-level output:

show_table("data/C1.genes.results", n = 3)
#>             gene_id  transcript_id(s) length effective_length expected_count
#> 1 ENSG00000000001.2 ENST00000000001.1    837              657             37
#> 2 ENSG00000000002.3 ENST00000000002.1    874              694             54
#> 3 ENSG00000000003.1 ENST00000000003.1    911              731             71
#>        TPM     FPKM
#> 1 3222.915 2578.332
#> 2 4504.587 3603.669
#> 3 5682.149 4545.719

1. Import

rsemflow data import --i-rsem-dir data/ --m-metadata metadata.tsv --o-study study/
#> Imported 6 samples and 200 genes into study/

Import checks that every RSEM file has the required columns, that file names and sample_id match one-to-one, and that all files share the same gene table. It then reads the counts with tximport and writes a study directory:

list.files("study")
#> [1] "effective_length.tsv" "expected_count.tsv"   "metadata.tsv"        
#> [4] "samples.tsv"          "study-info.tsv"       "study.rds"           
#> [7] "tpm.tsv"
show_table("study/study-info.tsv", n = 10)
#>                            key                   value
#> 1             rsemflow_version                   0.1.1
#> 2               format_version                       1
#> 3                   created_at 2026-10-06 00:37:32 UTC
#> 4                      samples                       6
#> 5                        genes                     200
#> 6 zero_effective_length_values                       0

expected_count.tsv, tpm.tsv, and effective_length.tsv are gene × sample matrices. study.rds is what later commands read.

age contains only numbers, so it was treated as numeric; condition and batch became categorical. If a numeric-looking column is really a set of groups (for example time points 0, 6, 24), add --p-categorical time to data import.

2. Expression summaries

rsemflow data summarize --i-study study/ --o-summary results/summary/
#> Wrote expression summaries to results/summary/
show_table("results/summary/samples.tsv")
#>   sample_id total_expected_count detected_genes undetected_genes
#> 1        C1                21880            200                0
#> 2        C2                21970            200                0
#> 3        C3                21960            200                0
#> 4        D1                27225            200                0
#> 5        D2                26585            200                0
#> 6        D3                26765            200                0
#>   detected_fraction  tpm_q25 tpm_median  tpm_q75 tpm_sum
#> 1                 1 2619.979   4326.391 6689.095   1e+06
#> 2                 1 2636.108   4416.054 6665.577   1e+06
#> 3                 1 2628.449   4448.008 6661.793   1e+06
#> 4                 1 1477.125   3068.127 5310.146   1e+06
#> 5                 1 1511.890   3073.025 5360.227   1e+06
#> 6                 1 1479.227   2939.596 5321.024   1e+06
show_table("results/summary/genes.tsv", n = 3)
#>             gene_id ensembl_gene_id mean_count median_count mean_tpm median_tpm
#> 1 ENSG00000000001.2 ENSG00000000001    186.125      169.625 12479.56   11436.16
#> 2 ENSG00000000002.3 ENSG00000000002    228.625      212.125 14745.73   13721.42
#> 3 ENSG00000000003.1 ENSG00000000003    271.125      254.625 16827.82   15821.05
#>   detected_samples detected_fraction
#> 1                6                 1
#> 2                6                 1
#> 3                6                 1

These are expression-level summaries computed from the RSEM tables, not alignment QC. rsemflow does not flag or remove samples; that decision stays with you.

3. Sample structure: PCA and correlation

rsemflow expression pca --i-study study/ --o-pca results/pca/
#> Warning: replacing previous import 'S4Arrays::makeNindexFromArrayViewport' by
#> 'DelayedArray::makeNindexFromArrayViewport' when loading 'SummarizedExperiment'
#> Wrote PCA tables to results/pca/

By default PCA uses the variance-stabilized counts (DESeq2 VST) of the 500 most variable genes (here all 200), centred and not scaled.

show_table("results/pca/scores.tsv", cols = c("sample_id", "PC1", "PC2", "PC3"))
#>   sample_id        PC1        PC2       PC3
#> 1        C1 -15.586657  9.0209984 -4.491250
#> 2        C2 -13.842807 -0.6441269  2.820268
#> 3        C3  -8.224491 -9.2509593  4.632941
#> 4        D1   8.610575 -6.3443824 -7.339276
#> 5        D2  13.626016  0.5887981 -1.590494
#> 6        D3  15.417364  6.6296722  5.967811
show_table("results/pca/variance.tsv", n = 3)
#>   component eigenvalue variance_fraction cumulative_variance
#> 1       PC1  199.94297        0.64846122           0.6484612
#> 2       PC2   50.38480        0.16340953           0.8118708
#> 3       PC3   28.31976        0.09184752           0.9037183

loadings.tsv holds the gene loadings and analysis-info.tsv records the transformation and number of genes used.

rsemflow expression correlation --i-study study/ --o-correlation results/correlation.tsv
#> Wrote sample correlation matrix to results/correlation.tsv
corr <- show_table("results/correlation.tsv")
corr[, -1] <- round(corr[, -1], 3)
corr
#>   sample_id     C1     C2    C3    D1     D2     D3
#> 1        C1  1.000  0.583 0.259 0.039 -0.093 -0.182
#> 2        C2  0.583  1.000 0.620 0.236  0.057 -0.075
#> 3        C3  0.259  0.620 1.000 0.471  0.255  0.080
#> 4        D1  0.039  0.236 0.471 1.000  0.812  0.649
#> 5        D2 -0.093  0.057 0.255 0.812  1.000  0.824
#> 6        D3 -0.182 -0.075 0.080 0.649  0.824  1.000

Use --p-method spearman or --p-transform log2-tpm to change the defaults.

4. Normalized expression

rsemflow expression normalize --i-study study/ --p-method vst --o-expression results/vst.tsv
#> Wrote expression matrix to results/vst.tsv

--p-method accepts vst, normalized-counts (DESeq2 size factors), and log2-tpm (log2(TPM + 1)).

5. Differential expression

The samples come from two batches, so batch goes into the model and the Drug-versus-Control effect is estimated within batch:

rsemflow differential deseq2 \
  --i-study study/ \
  --p-formula '~ batch + condition' \
  --p-term condition \
  --p-reference condition::Control \
  --o-differential results/differential/
#> Wrote 1 differential result table(s) to results/differential/
list.files("results/differential")
#> [1] "analysis-info.tsv"             "condition_Drug_vs_Control.tsv"
#> [3] "contrasts.tsv"                 "design-matrix.tsv"
de <- utils::read.delim("results/differential/condition_Drug_vs_Control.tsv")
de <- de[order(de$padj), ]
utils::head(de[, c("gene_id", "contrast", "baseMean", "log2FoldChange", "lfcSE", "stat", "padj")])
#>              gene_id        contrast  baseMean log2FoldChange     lfcSE
#> 33 ENSG00000000033.1 Drug vs Control  38.40338      -1.671021 0.2315823
#> 23 ENSG00000000023.3 Drug vs Control  45.12578      -1.774831 0.2524512
#> 22 ENSG00000000022.2 Drug vs Control  33.54292      -1.607113 0.2404466
#> 34 ENSG00000000034.2 Drug vs Control  49.82866      -1.825825 0.3096257
#> 30 ENSG00000000030.1 Drug vs Control 105.94716      -4.876302 0.8843351
#> 32 ENSG00000000032.3 Drug vs Control  26.84058      -1.428867 0.2612781
#>         stat         padj
#> 33 -7.215667 1.073405e-10
#> 23 -7.030393 2.059522e-10
#> 22 -6.683868 1.551447e-09
#> 34 -5.896878 1.852214e-07
#> 30 -5.514088 1.402374e-06
#> 32 -5.468760 1.510649e-06

The first six columns identify the comparison; the rest are the DESeq2 result columns, with unshrunken log2 fold changes. The three other files record exactly what was fitted:

show_table("results/differential/analysis-info.tsv", n = 10)
#>       key              value
#> 1 formula ~batch + condition
#> 2    term          condition
#> 3    test               wald
#> 4 reduced                   
#> 5   alpha               0.05
show_table("results/differential/contrasts.tsv")
#>                        name      term                 contrast coefficient
#> 1 condition_Drug_vs_Control condition condition::Drug::Control          NA
show_table("results/differential/design-matrix.tsv")
#>   sample_id (Intercept) batchB conditionDrug
#> 1        C1           1      0             0
#> 2        C2           1      1             0
#> 3        C3           1      0             0
#> 4        D1           1      1             1
#> 5        D2           1      0             1
#> 6        D3           1      1             1

For paired, multi-group, numeric, interaction, and time-course designs, see vignette("designs", package = "rsemflow").

6. Preranked GSEA

GSEA ranks genes by the DESeq2 Wald statistic (stat). With real human or mouse data, MSigDB collections are one option away:

rsemflow enrichment gsea \
  --i-differential results/differential/condition_Drug_vs_Control.tsv \
  --p-species human --p-collection hallmark \
  --o-enrichment results/gsea.tsv

The example genes are synthetic and match no MSigDB set, so here we use a custom GMT file: genes 1–20, genes 21–40, and genes 101–150.

ids <- sprintf("ENSG%011d", 1:200)
writeLines(c(
  paste(c("GENES_001_020", "synthetic", ids[1:20]), collapse = "\t"),
  paste(c("GENES_021_040", "synthetic", ids[21:40]), collapse = "\t"),
  paste(c("GENES_101_150", "synthetic", ids[101:150]), collapse = "\t")
), "sets.gmt")
rsemflow enrichment gsea \
  --i-differential results/differential/condition_Drug_vs_Control.tsv \
  --i-genesets sets.gmt \
  --p-min-size 10 \
  --o-enrichment results/gsea.tsv
#> Wrote 3 GSEA results to results/gsea.tsv
gsea <- utils::read.delim("results/gsea.tsv")
gsea$leadingEdge <- substr(gsea$leadingEdge, 1, 40)
gsea[, c("pathway", "size", "NES", "pval", "padj", "leadingEdge")]
#>         pathway size        NES         pval         padj
#> 1 GENES_021_040   20 -1.9646165 1.272006e-09 3.816017e-09
#> 2 GENES_001_020   20  2.4456215 5.528771e-09 8.293156e-09
#> 3 GENES_101_150   50 -0.8387051 7.555310e-01 7.555310e-01
#>                                leadingEdge
#> 1 ENSG00000000033;ENSG00000000023;ENSG0000
#> 2 ENSG00000000001;ENSG00000000012;ENSG0000
#> 3 ENSG00000000104;ENSG00000000136;ENSG0000

leadingEdge lists genes separated by ;. p-values are estimated by sampling, so rsemflow fixes the seed (--p-seed, default 42) and repeated runs give the same table.

7. GSVA and ssGSEA scores

rsemflow enrichment gsva \
  --i-study study/ \
  --i-genesets sets.gmt \
  --p-min-size 10 \
  --o-scores results/gsva.tsv
#> Warning: replacing previous import 'S4Arrays::makeNindexFromArrayViewport' by
#> 'DelayedArray::makeNindexFromArrayViewport' when loading 'HDF5Array'
#> Wrote 3 pathway score rows to results/gsva.tsv
gsva <- utils::read.delim("results/gsva.tsv", check.names = FALSE)
gsva <- gsva[, -(2:4)]
gsva[, -1] <- round(gsva[, -1], 3)
gsva
#>         pathway     C1     C2     C3     D1     D2     D3
#> 1 GENES_001_020 -0.017 -0.708 -0.810  0.870  0.690  0.200
#> 2 GENES_021_040  0.706  0.822  0.156 -0.234 -0.772 -0.817
#> 3 GENES_101_150 -0.066 -0.083  0.174 -0.173  0.010  0.097

Scores are computed per sample from log2(TPM + 1); add --p-method ssgsea for single-sample GSEA. rsemflow does not test scores between groups.

8. Gene annotation

Annotation adds gene symbols and biotypes while keeping the original RSEM gene_id. The most faithful source is the GTF used to build the RSEM reference:

rsemflow annotation gtf --i-study study/ --i-gtf Homo_sapiens.GRCh38.gtf.gz --o-annotation results/annotation.tsv

Without the GTF, the Bioconductor OrgDb packages map stable Ensembl IDs:

rsemflow annotation ensembl --i-study study/ --p-species human --o-annotation results/annotation.tsv

Join the annotation to any result table by gene_id.

Everything that was written

list.files("results", recursive = TRUE)
#>  [1] "correlation.tsv"                           
#>  [2] "differential/analysis-info.tsv"            
#>  [3] "differential/condition_Drug_vs_Control.tsv"
#>  [4] "differential/contrasts.tsv"                
#>  [5] "differential/design-matrix.tsv"            
#>  [6] "gsea.tsv"                                  
#>  [7] "gsva.tsv"                                  
#>  [8] "pca/analysis-info.tsv"                     
#>  [9] "pca/loadings.tsv"                          
#> [10] "pca/scores.tsv"                            
#> [11] "pca/variance.tsv"                          
#> [12] "summary/genes.tsv"                         
#> [13] "summary/samples.tsv"                       
#> [14] "vst.tsv"