# Introduction to densvis

Alan O'Callaghan*

*alan.ocallaghan@outlook.com

#### 17 July 2025

#### Package

densvis 1.19.0

# 1 Introduction

Non-linear dimensionality reduction techniques such as t-SNE (Maaten and Hinton 2008) and UMAP (McInnes, Healy, and Melville 2020) produce a low-dimensional embedding that summarises the global structure of high-dimensional data. These techniques can be particularly useful when visualising high-dimensional data in a biological setting. However, these embeddings may not accurately represent the local density of data in the original space, resulting in misleading visualisations where the space given to clusters of data does not represent the fraction of the high dimensional space that they occupy. `densvis` implements the density-preserving objective function described by (Narayan, Berger, and Cho 2020) which aims to address this deficiency by including a density-preserving term in the t-SNE and UMAP optimisation procedures. This can enable the creation of visualisations that accurately capture differing degrees of transcriptional heterogeneity within different cell subpopulations in scRNAseq experiments, for example.

# 2 Setting up the data

We will illustrate the use of densvis using simulated data. We will first load the `densvis` and `Rtsne` libraries and set a random seed to ensure the t-SNE visualisation is reproducible (note: it is good practice to ensure that a t-SNE embedding is robust by running the algorithm multiple times).

    library("densvis")
    library("Rtsne")
    library("uwot")
    library("ggplot2")
    theme_set(theme_bw())
    set.seed(14)

    data <- data.frame(
        x = c(rnorm(1000, 5), rnorm(1000, 0, 0.2)),
        y = c(rnorm(1000, 5), rnorm(1000, 0, 0.2)),
        class = c(rep("Class 1", 1000), rep("Class 2", 1000))
    )
    ggplot() +
        aes(data[, 1], data[, 2], colour = data$class) +
        geom_point(pch = 19) +
        scale_colour_discrete(name = "Cluster") +
        ggtitle("Original co-ordinates")

# 3 Running t-SNE

Density-preserving t-SNE can be generated using the `densne` function. This function returns a matrix of t-SNE co-ordinates. We set `dens_frac` (the fraction of optimisation steps that consider the density preservation) and `dens_lambda` (the weight given to density preservation relative to the standard t-SNE objective) each to 0.5.

    fit1 <- densne(data[, 1:2], dens_frac = 0.5, dens_lambda = 0.5)
    ggplot() +
        aes(fit1[, 1], fit1[, 2], colour = data$class) +
        geom_point(pch = 19) +
        scale_colour_discrete(name = "Class") +
        ggtitle("Density-preserving t-SNE") +
        labs(x = "t-SNE 1", y = "t-SNE 2")

If we run t-SNE on the same data, we can see that the density-preserving objective better represents the density of the data,

    fit2 <- Rtsne(data[, 1:2])
    ggplot() +
        aes(fit2$Y[, 1], fit2$Y[, 2], colour = data$class) +
        geom_point(pch = 19) +
        scale_colour_discrete(name = "Class") +
        ggtitle("Standard t-SNE") +
        labs(x = "t-SNE 1", y = "t-SNE 2")

# 4 Running UMAP

A density-preserving UMAP embedding can be generated using the `densmap` function. This function returns a matrix of UMAP co-ordinates. As with t-SNE, we set `dens_frac` (the fraction of optimisation steps that consider the density preservation) and `dens_lambda` (the weight given to density preservation relative to the standard t-SNE objective) each to 0.5.

    fit1 <- densmap(data[, 1:2], dens_frac = 0.5, dens_lambda = 0.5)
    #> Using Python: C:/Users/biocbuild/AppData/Local/r-reticulate/r-reticulate/pyenv/pyenv-win/versions/3.12.10/python.exe
    #> Creating virtual environment "C:\Users\biocbuild\AppData\Local/R/cache/R/basilisk/1.21.5/densvis/1.19.0/densvis" ...
    #> Done!
    #> Installing packages: pip, wheel, setuptools
    #> Installing packages: "umap-learn==0.5.9.post2", "scikit-learn==1.7.0", "numba==0.61.2", "pynndescent==0.5.13", "scipy==1.16.0", "numpy==2.2.6", "llvmlite==0.44.0"
    #> Virtual environment 'C:\Users\biocbuild\AppData\Local/R/cache/R/basilisk/1.21.5/densvis/1.19.0/densvis' successfully created.
    ggplot() +
        aes(fit1[, 1], fit1[, 2], colour = data$class) +
        geom_point(pch = 19) +
        scale_colour_discrete(name = "Class") +
        ggtitle("Density-preserving t-SNE") +
        labs(x = "t-SNE 1", y = "t-SNE 2")

If we run UMAP on the same data, we can see that the density-preserving objective better represents the density of the data,

    fit2 <- umap(data[, 1:2])
    ggplot() +
        aes(fit2[, 1], fit2[, 2], colour = data$class) +
        geom_point(pch = 19) +
        scale_colour_discrete(name = "Class") +
        ggtitle("Standard t-SNE") +
        labs(x = "t-SNE 1", y = "t-SNE 2")

# Session information

    sessionInfo()
    #> R version 4.5.1 (2025-06-13 ucrt)
    #> Platform: x86_64-w64-mingw32/x64
    #> Running under: Windows Server 2022 x64 (build 20348)
    #>
    #> Matrix products: default
    #>   LAPACK version 3.12.1
    #>
    #> locale:
    #> [1] LC_COLLATE=C
    #> [2] LC_CTYPE=English_United States.utf8
    #> [3] LC_MONETARY=English_United States.utf8
    #> [4] LC_NUMERIC=C
    #> [5] LC_TIME=English_United States.utf8
    #>
    #> time zone: America/New_York
    #> tzcode source: internal
    #>
    #> attached base packages:
    #> [1] stats     graphics  grDevices utils     datasets  methods   base
    #>
    #> other attached packages:
    #> [1] ggplot2_3.5.2    uwot_0.2.3       Matrix_1.7-3     Rtsne_0.17
    #> [5] densvis_1.19.0   BiocStyle_2.37.0
    #>
    #> loaded via a namespace (and not attached):
    #>  [1] gtable_0.3.6        jsonlite_2.0.0      dplyr_1.1.4
    #>  [4] compiler_4.5.1      BiocManager_1.30.26 filelock_1.0.3
    #>  [7] tinytex_0.57        tidyselect_1.2.1    Rcpp_1.1.0
    #> [10] FNN_1.1.4.1         magick_2.8.7        dichromat_2.0-0.1
    #> [13] parallel_4.5.1      assertthat_0.2.1    jquerylib_0.1.4
    #> [16] scales_1.4.0        png_0.1-8           yaml_2.3.10
    #> [19] fastmap_1.2.0       reticulate_1.42.0   lattice_0.22-7
    #> [22] R6_2.6.1            labeling_0.4.3      generics_0.1.4
    #> [25] knitr_1.50          tibble_3.3.0        bookdown_0.43
    #> [28] pillar_1.11.0       bslib_0.9.0         RColorBrewer_1.1-3
    #> [31] rlang_1.1.6         cachem_1.1.0        dir.expiry_1.17.0
    #> [34] xfun_0.52           sass_0.4.10         cli_3.6.5
    #> [37] withr_3.0.2         magrittr_2.0.3      digest_0.6.37
    #> [40] grid_4.5.1          rappdirs_0.3.3      basilisk_1.21.5
    #> [43] lifecycle_1.0.4     vctrs_0.6.5         evaluate_1.0.4
    #> [46] glue_1.8.0          farver_2.1.2        rmarkdown_2.29
    #> [49] pkgconfig_2.0.3     tools_4.5.1         htmltools_0.5.8.1

Maaten, Laurens van der, and Geoffrey Hinton. 2008. “Visualizing Data Using T-Sne.” Journal of Machine Learning Research 9 (Nov): 2579–2605.

McInnes, Leland, John Healy, and James Melville. 2020. “UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction.” http://arxiv.org/abs/1802.03426.

Narayan, Ashwin, Bonnie Berger, and Hyunghoon Cho. 2020. “Density-Preserving Data Visualization Unveils Dynamic Patterns of Single-Cell Transcriptomic Variability.” bioRxiv. https://doi.org/10.1101/2020.05.12.077776.