Skip to contents
RNGversion("3.5.0")
#> Warning in RNGkind("Mersenne-Twister", "Inversion", "Rounding"): non-uniform
#> 'Rounding' sampler used
SEED <- 7
set.seed(SEED)
N_CORES <- 4
if (ami::using_ci()) {
    N_CORES <- 2
}
bpparam <- MulticoreParam(N_CORES)

This article will show how BLASE can be used for excluding the effect of developmental differences when analysing bulk RNA-seq data. We make use of scRNA-seq Dogga et al. 2024 and bulk RNA-seq data from Zhang et al. 2021. Code for generating the objects used here is available in the BLASE reproducibility repository.

Load Data

First we’ll load in the data we’re using, pre-prepared from the BLASE reproducibility repository.

zhang_bulk <- readRDS("Data/zhang_2021_heat_shock_bulk.rds")
dogga_sc <- readRDS("Data/MCA_PF_SCE.rds")

We can examine the true lifecycle stages, and also the calculated pseudotime trajectory (Slingshot).

plotUMAP(dogga_sc, colour_by = "STAGE_HR")

UMAP of Dogga et al. single-cell RNA-seq reference coloured by lifecycle stage, showing the cycle from Rings, Trophozoites, Schizonts, to Rings again.

#| fig.alt: >
#|   UMAP of Dogga et al. single-cell RNA-seq reference coloured by pseudotime,
#|   starting with Rings, and ending with Schizonts.
plotUMAP(dogga_sc, colour_by = "slingPseudotime_1")

UMAP of Dogga et al. single-cell RNA-seq reference coloured by lifecycle stage, showing the cycle from Rings, Trophozoites, Schizonts, to Rings again. ## Prepare BLASE

Now we’ll prepare BLASE for use.

Create BLASE data object

First, we create the object, giving it the number of bins we want to use, and how to calculate them.

blase_data <- as.BlaseData(
    dogga_sc,
    pseudotime_slot = "slingPseudotime_1",
    n_bins = 8,
    split_by = "pseudotime_range"
)

# Add these bins to the sc metadata
dogga_sc <- blase::assign_pseudotime_bins(dogga_sc,
    pseudotime_slot = "slingPseudotime_1",
    n_bins = 8,
    split_by = "pseudotime_range"
)

Select Genes

Now we will select the genes we want to use, using BLASE’s gene peakedness metric.

gene_peakedness_info <- blase::calculate_gene_peakedness(
    dogga_sc,
    window_pct = 5,
    knots = 18,
    BPPARAM = bpparam
)

genes_to_use <- blase::gene_peakedness_spread_selection(
    dogga_sc,
    gene_peakedness_info,
    genes_per_bin = 30,
    n_gene_bins = 30
)

head(gene_peakedness_info)
#>              gene peak_pseudotime mean_in_window mean_out_window     ratio
#> 72  PF3D7-1401100        9.811180       5.946666        1.576877  3.771165
#> 57  PF3D7-1401200        7.767184       4.928158        1.981323  2.487307
#> 59  PF3D7-1401300        8.039717       2.207782        1.285688  1.717199
#> 721 PF3D7-1401400        9.811180     942.073177       49.944485 18.862406
#> 15  PF3D7-1401600        2.043996      96.706165        8.207937 11.782031
#> 19  PF3D7-1402200        2.589061       3.516862        1.549402  2.269819
#>     window_start window_end deviance_explained
#> 72      9.470514  10.151846         0.16478147
#> 57      7.426518   8.107850         0.19846899
#> 59      7.699051   8.380383         0.04479178
#> 721     9.470514  10.151846         0.27186813
#> 15      1.703330   2.384662         0.64855890
#> 19      2.248395   2.929727         0.17093181

Here, we add them to the BLASE object for mapping with.

genes(blase_data) <- genes_to_use

Calculate Mappings

Now we can perform the actual mapping step, and review the results.

mapping_results <- blase::map_all_best_bins(
    blase_data,
    zhang_bulk,
    BPPARAM = bpparam
)
blase::plot_mapping_result_heatmap(mapping_results)

Heatmap of mapping correlations of the Zhang et al. data onto the Dogga et al. scRNA-seq.

Calculate DE genes

We calculate DE genes using Limma (ref) as we have access to only normalised counts.

metadata <- data.frame(row.names = seq_len(12))
metadata$strain <- c(
    rep("NF54", 4),
    rep("PB4", 4),
    rep("PB31", 4)
)
metadata$growth_conditions <- rep(c("Normal", "Normal", "HS", "HS"), 3)
metadata$group <- paste0(metadata$strain, "_", metadata$growth_conditions)
rownames(metadata) <- c(
    "NF54_37_1",
    "NF54_37_2",
    "NF54_41_1",
    "NF54_41_2",
    "PB4_37_1",
    "PB4_37_2",
    "PB4_41_1",
    "PB4_41_2",
    "PB31_37_1",
    "PB31_37_2",
    "PB31_41_1",
    "PB31_41_2"
)

design <- model.matrix(~ 0 + group, metadata)
colnames(design) <- gsub("group", "", colnames(design))

contr.matrix <- makeContrasts(
    WT_normal_v_HS = NF54_Normal - NF54_HS,
    PB31_normal_v_HS = PB31_Normal - PB31_HS,
    normal_NF54_v_PB31 = NF54_Normal - PB31_Normal,
    levels = colnames(design)
)

Phenotype + Development (Bulk DE)

vfit <- lmFit(zhang_bulk, design)
vfit <- contrasts.fit(vfit, contrasts = contr.matrix)
efit <- eBayes(vfit)

summary(decideTests(efit))
#>        WT_normal_v_HS PB31_normal_v_HS normal_NF54_v_PB31
#> Down              289              328                229
#> NotSig           2188             1884               2231
#> Up                 80              345                 97

PB31_normal_v_HS_BULK_DE <- topTable(efit, n = Inf, coef = 2)
PB31_normal_v_HS_BULK_DE <- PB31_normal_v_HS_BULK_DE[PB31_normal_v_HS_BULK_DE$adj.P.Val < 0.05 & PB31_normal_v_HS_BULK_DE$logFC > 0, ]
PB31_normal_v_HS_BULK_DE <- PB31_normal_v_HS_BULK_DE[order(-PB31_normal_v_HS_BULK_DE$logFC), ]

Development (SC DE)

First, let’s pseudobulk based on the pseudotime bins

pseudobulked_dogga_sc <- data.frame(row.names = rownames(dogga_sc))
for (r in mapping_results) {
    pseudobulked_dogga_sc[bulk_name(r)] <- rowSums(counts(dogga_sc[, dogga_sc$pseudotime_bin == best_bin(r)]))
}
pseudobulked_dogga_sc <- as.matrix(pseudobulked_dogga_sc)

And now calculate DE genes for these

## Normalise, not needed for true bulks
par(mfrow = c(1, 2))
v <- voom(pseudobulked_dogga_sc, design, plot = FALSE)

vfit_sc <- lmFit(v, design)
vfit_sc <- contrasts.fit(vfit_sc, contrasts = contr.matrix)
efit_sc <- eBayes(vfit_sc)

summary(decideTests(efit_sc))
#>        WT_normal_v_HS PB31_normal_v_HS normal_NF54_v_PB31
#> Down                0                0                  0
#> NotSig           1768             1768               1768
#> Up                  0                0                  0
PB31_normal_v_HS_SC_DE <- topTable(efit_sc, n = Inf, coef = 2)
PB31_normal_v_HS_SC_DE <- PB31_normal_v_HS_SC_DE[PB31_normal_v_HS_SC_DE$adj.P.Val < 0.05 & PB31_normal_v_HS_SC_DE$logFC > 0, ]
PB31_normal_v_HS_SC_DE <- PB31_normal_v_HS_SC_DE[order(-PB31_normal_v_HS_SC_DE$logFC), ]

Remove Developmental Genes

How many of these genes overlap? We can look at the intersection using a Venn Diagram:

ggVennDiagram(list(
    Phenotype = rownames(PB31_normal_v_HS_BULK_DE),
    Development = rownames(PB31_normal_v_HS_SC_DE)
)) + ggtitle("PB31 Normal vs Heat Shock")

Venn Diagram showing the intersection of Bulk and SC DE genes.

Great, there are genes which we can correct. We can now remove these from the original DE genes list.

NB: This is a primitive approach, and there are likely many other and better ways to calculate which genes should be kept or removed.

PB31_normal_v_HS_corrected_DE <- rownames(PB31_normal_v_HS_BULK_DE[!(rownames(PB31_normal_v_HS_BULK_DE) %in% rownames(PB31_normal_v_HS_SC_DE)), ])
head(PB31_normal_v_HS_corrected_DE)
#> [1] "PF3D7-1003600" "PF3D7-1310700" "PF3D7-1035200" "PF3D7-0714000"
#> [5] "PF3D7-0725400" "PF3D7-1035500"

Further downstream analysis can now be performed, as desired by the researcher, for example Gene Ontology term enrichment.

Session Info

sessionInfo()
#> R version 4.5.1 (2025-06-13)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.3 LTS
#> 
#> Matrix products: default
#> BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
#> 
#> Random number generation:
#>  RNG:     Mersenne-Twister 
#>  Normal:  Inversion 
#>  Sample:  Rounding 
#>  
#> locale:
#>  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
#>  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
#>  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
#> [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
#> 
#> time zone: UTC
#> tzcode source: system (glibc)
#> 
#> attached base packages:
#> [1] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] ggVennDiagram_1.5.4         limma_3.64.3               
#>  [3] blase_0.99.0                BiocParallel_1.42.1        
#>  [5] scater_1.36.0               ggplot2_3.5.2              
#>  [7] scuttle_1.18.0              SingleCellExperiment_1.30.1
#>  [9] SummarizedExperiment_1.38.1 Biobase_2.68.0             
#> [11] GenomicRanges_1.60.0        GenomeInfoDb_1.44.2        
#> [13] IRanges_2.42.0              S4Vectors_0.46.0           
#> [15] BiocGenerics_0.54.0         generics_0.1.4             
#> [17] MatrixGenerics_1.20.0       matrixStats_1.5.0          
#> 
#> loaded via a namespace (and not attached):
#>  [1] tidyselect_1.2.1         ami_0.2.1                viridisLite_0.4.2       
#>  [4] dplyr_1.1.4              vipor_0.4.7              farver_2.1.2            
#>  [7] viridis_0.6.5            fastmap_1.2.0            digest_0.6.37           
#> [10] rsvd_1.0.5               lifecycle_1.0.4          statmod_1.5.0           
#> [13] magrittr_2.0.3           compiler_4.5.1           rlang_1.1.6             
#> [16] sass_0.4.10              tools_4.5.1              yaml_2.3.10             
#> [19] knitr_1.50               labeling_0.4.3           S4Arrays_1.8.1          
#> [22] htmlwidgets_1.6.4        DelayedArray_0.34.1      RColorBrewer_1.1-3      
#> [25] abind_1.4-8              withr_3.0.2              desc_1.4.3              
#> [28] grid_4.5.1               beachmat_2.24.0          scales_1.4.0            
#> [31] cli_3.6.5                rmarkdown_2.29           crayon_1.5.3            
#> [34] ragg_1.5.0               httr_1.4.7               ggbeeswarm_0.7.2        
#> [37] cachem_1.1.0             splines_4.5.1            parallel_4.5.1          
#> [40] XVector_0.48.0           vctrs_0.6.5              boot_1.3-31             
#> [43] Matrix_1.7-3             jsonlite_2.0.0           BiocSingular_1.24.0     
#> [46] patchwork_1.3.2          BiocNeighbors_2.2.0      ggrepel_0.9.6           
#> [49] irlba_2.3.5.1            beeswarm_0.4.0           systemfonts_1.2.3       
#> [52] jquerylib_0.1.4          glue_1.8.0               pkgdown_2.1.3           
#> [55] codetools_0.2-20         cowplot_1.2.0            gtable_0.3.6            
#> [58] UCSC.utils_1.4.0         ScaledMatrix_1.16.0      tibble_3.3.0            
#> [61] pillar_1.11.0            htmltools_0.5.8.1        GenomeInfoDbData_1.2.14 
#> [64] R6_2.6.1                 sparseMatrixStats_1.20.0 textshaping_1.0.3       
#> [67] evaluate_1.0.5           lattice_0.22-7           bslib_0.9.0             
#> [70] Rcpp_1.1.0               gridExtra_2.3            SparseArray_1.8.1       
#> [73] nlme_3.1-168             mgcv_1.9-3               xfun_0.53               
#> [76] fs_1.6.6                 pkgconfig_2.0.3