{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "commercial-typing",
      "metadata": {
        "id": "commercial-typing"
      },
      "source": [
        "# scRNA Analysis Preprocessing Workflow\n",
        "\n",
        "**Author:** [Severin Dicks](https://github.com/Intron7)\n",
        "**Copyright** [scverse](https://scverse.org)\n",
        "\n",
        "\n",
        "This notebook demonstrates a complete end-to-end single cell analysis workflow. It starts with loading the dataset with over 200k cells from the [CELLxGENE data repository](https://cellxgene.cziscience.com/e/ae29ebd0-1973-40a4-a6af-d15a5f77a80f.cxg/). We will use [AnnData](https://anndata.readthedocs.io/en/stable/) to load the dataset into Python, [scanpy](https://scanpy.readthedocs.io/en/stable/) for plotting, and [RAPIDS-singlecell](https://rapids-singlecell.readthedocs.io/en/latest/) to run data processing on the GPU. The workflow for the labs is as follows:\n",
        "\n",
        "1. Data loading\n",
        "2. Data pre-processing\n",
        "    1. Quality Control (QC) to visually understand the data\n",
        "    2. Filtering unusual cells\n",
        "    3. Normalization to removing unwanted variation\n",
        "3. Clustering and visualization:\n",
        "    1. PCA\n",
        "    2. Nearest Neighbors and UMAP\n",
        "    3. Louvain and Leiden Clustering\n",
        "    4. Batch Correction\n",
        "    5. t-SNE\n",
        "    6. Differential expression analysis\n",
        "    7. Diffusion Map Embedding\n",
        "\n",
        "**Let's begin!**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "fabulous-ultimate",
      "metadata": {
        "id": "fabulous-ultimate",
        "outputId": "fc3538c6-bdf1-4949-dfb4-f19f570cf09c",
        "tags": []
      },
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "/opt/conda/lib/python3.13/site-packages/scanpy/_utils/__init__.py:33: FutureWarning: `__version__` is deprecated, use `importlib.metadata.version('anndata')` instead.\n",
            "  from anndata import __version__ as anndata_version\n",
            "/opt/conda/lib/python3.13/site-packages/scanpy/__init__.py:24: FutureWarning: `__version__` is deprecated, use `importlib.metadata.version('anndata')` instead.\n",
            "  if Version(anndata.__version__) >= Version(\"0.11.0rc2\"):\n",
            "/opt/conda/lib/python3.13/site-packages/scanpy/readwrite.py:16: FutureWarning: `__version__` is deprecated, use `importlib.metadata.version('anndata')` instead.\n",
            "  if Version(anndata.__version__) >= Version(\"0.11.0rc2\"):\n"
          ]
        }
      ],
      "source": [
        "import scanpy as sc\n",
        "import cupy as cp\n",
        "\n",
        "import time\n",
        "import rapids_singlecell as rsc\n",
        "\n",
        "import warnings\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "wAYVVrzNkGSW",
      "metadata": {
        "id": "wAYVVrzNkGSW"
      },
      "source": [
        "Before we begin, we must set up the RAPIDS Memory Manager, which will determine how the dataset is stored in VRAM. We can choose either `managed_memory` (if the dataset does not fit into VRAM) or `pool_allocator` (if the dataset does fit into VRAM). Since the dataset for this notebook is small, it should fit inside the VRAM of most GPUs, so we will set `pool_allocator=True` and `managed_memory=False`. Only one option can be set true at a time, not both. For more information on the difference between these two options, see the [RAPIDS docs](https://rapids-singlecell.readthedocs.io/en/latest/MM.html#memory-management)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "twenty-orbit",
      "metadata": {
        "id": "twenty-orbit"
      },
      "outputs": [],
      "source": [
        "import rmm\n",
        "from rmm.allocators.cupy import rmm_cupy_allocator\n",
        "\n",
        "rmm.reinitialize(\n",
        "    managed_memory=False,  # Allows oversubscription\n",
        "    pool_allocator=True,  # default is False\n",
        "    devices=0,  # GPU device IDs to register. By default registers only GPU 0.\n",
        ")\n",
        "cp.cuda.set_allocator(rmm_cupy_allocator)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "scenic-overview",
      "metadata": {
        "id": "scenic-overview"
      },
      "source": [
        "## Data Loading"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "742c6b09-41cf-4add-bf49-89cd44d77b17",
      "metadata": {
        "id": "742c6b09-41cf-4add-bf49-89cd44d77b17"
      },
      "source": [
        "In this lab, we will use a single cell dataset that can be found in the [CELLxGENE](https://cellxgene.cziscience.com/e/ae29ebd0-1973-40a4-a6af-d15a5f77a80f.cxg/) data repository. It contains over 200k cells and 60k genes taken from immune cells across multiple tissue types."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "6f2940f0-a569-4b49-8c56-c1f8a7e09ef1",
      "metadata": {
        "id": "6f2940f0-a569-4b49-8c56-c1f8a7e09ef1",
        "outputId": "da981344-09f3-447c-a030-e05142dab44a"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "./h5 directory found\n",
            "./h5/dli_census.h5ad dataset found\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "import anndata as ad\n",
        "import wget\n",
        "\n",
        "url = 'https://scverse-exampledata.s3.eu-west-1.amazonaws.com/rapids-singlecell/dli_census.h5ad'\n",
        "data_dir = \"./h5\"\n",
        "output = data_dir+'/dli_census.h5ad'\n",
        "if not os.path.exists(data_dir): # Check if h5 directory exists\n",
        "    print('creating data directory')\n",
        "    os.system('mkdir ./h5')\n",
        "else:\n",
        "    print(f'{data_dir} directory found')\n",
        "\n",
        "if not os.path.isfile(output): # Check to see if we have our final output.  If it is there, get to the analysis!\n",
        "    if not os.path.isfile(output): # as it's not there, let's see if we have our downloaded file.  If not, let's get it!\n",
        "        print('Downloading cell data..')\n",
        "        wget.download(url, output)\n",
        "\n",
        "    adata = ad.read_h5ad(output)\n",
        "    adata= adata[adata.obs[\"assay\"].isin([\"10x 3' v3\", \"10x 5' v1\", \"10x 5' v2\"])].copy()\n",
        "    adata.write(output)\n",
        "else:\n",
        "    print(f'{output} dataset found')"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "saved-plain",
      "metadata": {
        "id": "saved-plain"
      },
      "source": [
        "Load the `h5ad` file containing the sparse count matrix into an AnnData object. See the [AnnData documentation](https://anndata.readthedocs.io/en/stable/) for more information on this data format."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "comic-fundamental",
      "metadata": {
        "id": "comic-fundamental",
        "outputId": "78ea7efc-5494-43a3-9f8d-03efcca8970c"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 353 ms, sys: 837 ms, total: 1.19 s\n",
            "Wall time: 1.19 s\n"
          ]
        }
      ],
      "source": [
        "adata = sc.read(output)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "lOA4gWcDnyBB",
      "metadata": {
        "id": "lOA4gWcDnyBB"
      },
      "source": [
        "By default, this data object is loading onto the CPU. We will use RAPIDS-singlecell to move the AnnData object to the GPU."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "assured-premiere",
      "metadata": {
        "id": "assured-premiere",
        "outputId": "483896b0-79ad-4e57-9e10-6017828b89d0",
        "tags": []
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 115 ms, sys: 784 ms, total: 899 ms\n",
            "Wall time: 897 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.get.anndata_to_GPU(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "765XMX2Et0eM",
      "metadata": {
        "id": "765XMX2Et0eM"
      },
      "source": [
        "Note the initial size of the dataset"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "Y8YcQFykt0wT",
      "metadata": {
        "id": "Y8YcQFykt0wT"
      },
      "outputs": [],
      "source": [
        "adata.shape"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "hawaiian-cooperation",
      "metadata": {
        "id": "hawaiian-cooperation"
      },
      "source": [
        "## Data Preprocessing"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "Sr02WrudowzX",
      "metadata": {
        "id": "Sr02WrudowzX"
      },
      "source": [
        "As a prerequisite for downstream analysis, it will help if the variable names"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "72d4309d-6a95-4193-9ac8-8687b81b7698",
      "metadata": {
        "id": "72d4309d-6a95-4193-9ac8-8687b81b7698"
      },
      "outputs": [],
      "source": [
        "adata.var_names = adata.var.feature_name.astype(str).to_numpy()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "peripheral-senate",
      "metadata": {
        "id": "peripheral-senate"
      },
      "source": [
        "### Quality Control"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "presidential-parts",
      "metadata": {
        "id": "presidential-parts"
      },
      "source": [
        "We calculate quality control (QC) metrics to assess cell and gene quality. These include:  \n",
        "\n",
        "- **Per cell metrics**:  \n",
        "  - Total counts per cell (library size)  \n",
        "  - Number of detected genes per cell  \n",
        "  - Percentage of counts from mitochondrial (`MT`) and ribosomal (`RIBO`) genes  \n",
        "\n",
        "- **Per gene metrics** (gene space):  \n",
        "  - Total counts per gene  \n",
        "  - Number of cells expressing each gene  \n",
        "\n",
        "These metrics help identify low-quality or stressed cells and ensure a meaningful feature set for downstream analysis."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "eMYoCt8IqA-N",
      "metadata": {
        "id": "eMYoCt8IqA-N"
      },
      "source": [
        "To count the mitochondrial (MT) and ribosomal (RIBO) genes, we must first flag which genes belong to these groups. This can be done by creating a boolean column with the name set by `gene_family_name` where the value is true for any genes with the prefix set by `gene_family_prefix`. This column will be added in place. For more information see the docs for [`rsc.pp.flag_gene_family`](https://rapids-singlecell.readthedocs.io/en/latest/api/generated/rapids_singlecell.pp.flag_gene_family.html#rapids_singlecell.pp.flag_gene_family)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "greatest-artwork",
      "metadata": {
        "id": "greatest-artwork",
        "outputId": "ee7c1e22-8d7c-4bc0-ce0e-5a01a87e719c"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 2.78 ms, sys: 0 ns, total: 2.78 ms\n",
            "Wall time: 2.57 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.flag_gene_family(adata, gene_family_name=\"MT\", gene_family_prefix=\"MT\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "major-brisbane",
      "metadata": {
        "id": "major-brisbane",
        "outputId": "be9020c7-369b-424f-e5e3-12b2486c10e2"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 2.14 ms, sys: 89 μs, total: 2.23 ms\n",
            "Wall time: 2.21 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.flag_gene_family(adata, gene_family_name=\"RIBO\", gene_family_prefix=\"RPS\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "k1xjpeqKrCNw",
      "metadata": {
        "id": "k1xjpeqKrCNw"
      },
      "source": [
        "Lastly, we can calculate the QC metrics needed for visualization by passing in our new column names to the [`rsc.pp.calculate_qc_metrics`](https://rapids-singlecell.readthedocs.io/en/latest/api/generated/rapids_singlecell.pp.calculate_qc_metrics.html) function."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "weighted-pound",
      "metadata": {
        "id": "weighted-pound",
        "outputId": "53175fac-766f-42a2-9bf2-cfe332722e5f"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 453 ms, sys: 24.4 ms, total: 477 ms\n",
            "Wall time: 556 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.calculate_qc_metrics(adata, qc_vars=[\"MT\", \"RIBO\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d32205f0",
      "metadata": {
        "id": "d32205f0"
      },
      "source": [
        "To visualize the quality control (QC) metrics, we generate the following plots:\n",
        "\n",
        "1. **Scatter plot: Total counts vs. Mitochondrial percentage**  \n",
        "   - This plot shows the relationship between the total UMI counts per cell and the percentage of mitochondrial (`MT`) gene expression.  \n",
        "   - Cells with high mitochondrial percentages may indicate stressed or dying cells.\n",
        "\n",
        "2. **Scatter plot: Total counts vs. Number of detected genes**  \n",
        "   - This plot displays the correlation between the total UMI counts per cell and the number of detected genes.  \n",
        "   - A strong correlation is expected, but outliers with low gene counts might indicate empty droplets or dead cells.\n",
        "\n",
        "3. **Violin plot: Number of detected genes per cell**  \n",
        "   - This violin plot visualizes the distribution of the number of detected genes per cell.  \n",
        "   - It helps identify cells with abnormally low or high gene counts, which could be filtered out.\n",
        "\n",
        "4. **Violin plot: Total counts per cell**  \n",
        "   - This plot shows the distribution of total counts per cell, indicating library size variation.  \n",
        "   - Extreme values may suggest low-quality or overly dominant cells.\n",
        "\n",
        "5. **Violin plot: Percentage of mitochondrial counts per cell**  \n",
        "   - This plot illustrates the distribution of mitochondrial gene expression across all cells.  \n",
        "   - High mitochondrial content could be a sign of cell stress or apoptosis.\n",
        "\n",
        "These visualizations help assess dataset quality and guide decisions on filtering low-quality cells."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "70932e9c-6ce6-4de0-808f-fc88ce7ea512",
      "metadata": {
        "id": "70932e9c-6ce6-4de0-808f-fc88ce7ea512",
        "outputId": "2e229ab3-b090-456e-d795-233ea7dd3f1b",
        "scrolled": true
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 97140 characters]",
            "text/plain": [
              "<Figure size 728.72x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "[encoded data, 70188 characters]",
            "text/plain": [
              "<Figure size 728.72x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sc.pl.scatter(adata, x=\"total_counts\", y=\"pct_counts_MT\")\n",
        "sc.pl.scatter(adata, x=\"total_counts\", y=\"n_genes_by_counts\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "adolescent-burlington",
      "metadata": {
        "id": "adolescent-burlington"
      },
      "source": [
        "### Filtering"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "developmental-fluid",
      "metadata": {
        "id": "developmental-fluid"
      },
      "source": [
        "When gene counts (`n_genes_by_counts`) are too large or the percentage of mitochondrial dna (`pct_counts_MT`) is too high, it can be indicative of low quality cells which can be filtered out of the dataset. The cut-offs are determined by visual inspection of the scatter plots."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "apart-faith",
      "metadata": {
        "id": "apart-faith",
        "outputId": "2df0a199-7c0a-4fbe-a879-94459f256912"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 433 ms, sys: 107 ms, total: 540 ms\n",
            "Wall time: 539 ms\n"
          ]
        }
      ],
      "source": [
        "%%time\n",
        "adata = adata[adata.obs[\"n_genes_by_counts\"] < 5000]\n",
        "adata = adata[adata.obs[\"pct_counts_MT\"] < 20]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "spiritual-filing",
      "metadata": {
        "id": "spiritual-filing"
      },
      "source": [
        "We also filter out genes that are expressed in less than 3 cells, as they will not provide much information."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "municipal-buyer",
      "metadata": {
        "id": "municipal-buyer",
        "outputId": "02650fe9-9a88-41a9-be61-a28db537c962",
        "scrolled": true
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "filtered out 8265 genes that are detected in less than 3 cells\n",
            "CPU times: user 2.09 s, sys: 316 ms, total: 2.41 s\n",
            "Wall time: 2.88 s\n"
          ]
        }
      ],
      "source": [
        "%%time\n",
        "rsc.pp.filter_genes(adata, min_cells=3)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "juvenile-inside",
      "metadata": {
        "id": "juvenile-inside"
      },
      "source": [
        "The size of our count matrix is now reduced."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "photographic-hobby",
      "metadata": {
        "id": "photographic-hobby",
        "outputId": "cc02903b-e251-4117-e9a1-a495c3d4ae9f"
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "(216382, 28133)"
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "adata.shape"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "armed-scratch",
      "metadata": {
        "id": "armed-scratch"
      },
      "source": [
        "### Normalization"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "wicked-whole",
      "metadata": {
        "id": "wicked-whole"
      },
      "source": [
        "We normalize the count matrix so that the total counts in each cell sum to 1e4 using a [shifted logarithm](https://www.sc-best-practices.org/preprocessing_visualization/normalization.html#shifted-logarithm). This will help remove some technical variability due to sequencing depth."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "chemical-chair",
      "metadata": {
        "id": "chemical-chair",
        "outputId": "073ebe15-28d1-4f4f-d8c6-689d967414a9"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 31.1 ms, sys: 23 μs, total: 31.1 ms\n",
            "Wall time: 31.1 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.normalize_total(adata, target_sum=1e4)\n",
        "rsc.pp.log1p(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "celtic-presence",
      "metadata": {
        "id": "celtic-presence"
      },
      "source": [
        "**Select Most Variable Genes**"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "693e9276-100f-469c-a7f1-452c32c61bfb",
      "metadata": {
        "id": "693e9276-100f-469c-a7f1-452c32c61bfb"
      },
      "source": [
        "We now search for highly variable genes, which help focus our analysis on the most informative genes while improving computational efficiency. This function supports the flavors `cell_ranger`, `seurat`, `seurat_v3`, and `pearson_residuals`.  \n",
        "As in Scanpy, you can either filter based on variance cutoffs or select the `n_top_genes`. Additionally, you can use a `batch_key` to correct for batch effects.  \n",
        "\n",
        "In this example, we use the `cell_ranger` method, which selects highly variable genes based on the log-normalized counts stored in `.X`.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "satellite-criterion",
      "metadata": {
        "id": "satellite-criterion",
        "outputId": "3133f286-2bd5-425e-94b2-fe4adbd361ed"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 318 ms, sys: 10.2 ms, total: 328 ms\n",
            "Wall time: 350 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.highly_variable_genes(adata, n_top_genes=5000, flavor=\"cell_ranger\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "arctic-upgrade",
      "metadata": {
        "id": "arctic-upgrade"
      },
      "source": [
        "Now we save this version of the AnnData as adata.raw."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "falling-soldier",
      "metadata": {
        "id": "falling-soldier",
        "outputId": "db91f32a-ba14-42f5-e2fb-14dbde0930f6"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 1.69 ms, sys: 132 ms, total: 134 ms\n",
            "Wall time: 183 ms\n"
          ]
        }
      ],
      "source": [
        "adata.raw = adata"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "north-concept",
      "metadata": {
        "id": "north-concept"
      },
      "source": [
        "We now restrict our AnnData object to only the highly variable genes.  \n",
        "This step reduces the number of features, focusing the analysis on the most informative genes while improving computational efficiency."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "peripheral-annotation",
      "metadata": {
        "id": "peripheral-annotation",
        "outputId": "14c377cb-42ff-4e7a-de48-8797b4b7e2b2"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 302 ms, sys: 121 ms, total: 422 ms\n",
            "Wall time: 742 ms\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.filter_highly_variable(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "seventh-liquid",
      "metadata": {
        "id": "seventh-liquid"
      },
      "source": [
        "Next, we regress out the effects of total counts per cell and mitochondrial content.  \n",
        "This helps remove technical variation that could bias downstream analyses.  \n",
        "\n",
        "As in Scanpy, you can regress out any numerical column from `.obs`, allowing for the correction of batch effects, sequencing depth, or other confounding factors.\n",
        "\n",
        "For more complex data sets, these steps may overcorrect and remove potentially useful information and need to be carefully used."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "organizational-judgment",
      "metadata": {
        "id": "organizational-judgment",
        "outputId": "18d9a316-b9d5-459e-8b45-dc5ea48a5c22"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 788 ms, sys: 141 ms, total: 929 ms\n",
            "Wall time: 1.6 s\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.regress_out(adata, keys=[\"total_counts\", \"pct_counts_MT\"])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "simple-change",
      "metadata": {
        "id": "simple-change"
      },
      "source": [
        "**Scale**"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "monetary-burke",
      "metadata": {
        "id": "monetary-burke"
      },
      "source": [
        "Finally, we scale the count matrix to obtain a z-score transformation, standardizing gene expression across cells.  \n",
        "To prevent extreme values from dominating the analysis, we apply a cutoff of 10 standard deviations.  \n",
        "This ensures that highly expressed genes do not disproportionately influence downstream computations."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "visible-explanation",
      "metadata": {
        "id": "visible-explanation",
        "outputId": "913e2acf-bfa6-4266-9981-2fc8ef8b786d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 1.12 s, sys: 189 ms, total: 1.31 s\n",
            "Wall time: 1.71 s\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.scale(adata, max_value=10)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "delayed-combination",
      "metadata": {
        "id": "delayed-combination"
      },
      "source": [
        "Now we move `.X` out of the GPU."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "lightweight-breeding",
      "metadata": {
        "id": "lightweight-breeding",
        "outputId": "51c2ad5a-cdee-43cf-8209-ea3fd117ce55"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Total Preprocessing time: 12.143767356872559\n"
          ]
        }
      ],
      "source": [
        "preprocess_time = time.time()\n",
        "print(\"Total Preprocessing time: %s\" % (preprocess_time - preprocess_start))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "first-reggae",
      "metadata": {
        "id": "first-reggae"
      },
      "source": [
        "## Clustering and Visualization"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "early-feelings",
      "metadata": {
        "id": "early-feelings"
      },
      "source": [
        "### Principal component analysis"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "available-trauma",
      "metadata": {
        "id": "available-trauma"
      },
      "source": [
        "We use Principal Component Analysis (PCA) to reduce the dimensionality of the gene expression matrix to its top 100 principal components.  \n",
        "This step captures the most important sources of variation in the data while reducing computational complexity.  \n",
        "\n",
        "We use the GPU-accelerated PCA implementation from cuML, which significantly speeds up computation compared to CPU-based methods."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "virtual-insertion",
      "metadata": {
        "id": "virtual-insertion",
        "outputId": "173b026f-164d-4923-e015-dedb8b94bca2"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 618 ms, sys: 330 ms, total: 948 ms\n",
            "Wall time: 4.19 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.pca(adata, n_comps=100)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "pretty-cheese",
      "metadata": {
        "id": "pretty-cheese"
      },
      "source": [
        "We can use scanpy `pca_variance_ratio` plot to inspect the contribution of single PCs to the total variance in the data."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "statewide-disposal",
      "metadata": {
        "id": "statewide-disposal",
        "outputId": "5aeb49ca-cc30-435e-b129-0999a6a96160"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 45668 characters]",
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sc.pl.pca_variance_ratio(adata, log=True, n_pcs=100)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "92674c3f-82c7-4baa-8019-d19c3710de66",
      "metadata": {
        "id": "92674c3f-82c7-4baa-8019-d19c3710de66"
      },
      "source": [
        "Now, we transfer the `.X` matrix back to host (CPU) memory to free up GPU resources.  \n",
        "\n",
        "Since some downstream analyses or visualizations may not require GPU acceleration, this step helps optimize memory usage,  \n",
        "preventing unnecessary GPU load while still keeping the processed data accessible for further analysis."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f62c276a-4530-4044-9427-a1a5e66e6094",
      "metadata": {
        "id": "f62c276a-4530-4044-9427-a1a5e66e6094"
      },
      "outputs": [],
      "source": [
        "rsc.get.anndata_to_CPU(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "imported-meeting",
      "metadata": {
        "id": "imported-meeting"
      },
      "source": [
        "### Nearest Neighbors and UMAP"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "biological-phenomenon",
      "metadata": {
        "id": "biological-phenomenon"
      },
      "source": [
        "Next, we compute the neighborhood graph using `rsc`.  \n",
        "\n",
        "Scanpy’s CPU-based implementation of nearest neighbor search uses an approximation, while the GPU-accelerated RAPIDS implementation computes the exact graph.  \n",
        "Both methods are valid, but small differences in results can occur.  \n",
        "\n",
        "The following approximate nearest neighbor (ANN) algorithms are also supported:  \n",
        "- **`ivfflat`**: Uses an inverted file index for fast approximate search, suitable for very large datasets.  \n",
        "- **`ivfpq`**: A variant of `ivfflat` that compresses data for even more efficient memory usage, trading off some accuracy.  \n",
        "- **`cagra`**: A graph-based ANN method optimized for fast, high-accuracy queries on GPUs.  \n",
        "- **`nn_descent`**: A graph-based method that incrementally refines the nearest neighbor search and works well for large, high-dimensional datasets.  \n",
        "\n",
        "Since our dataset is relatively small, we use **brute-force (`brute`)** search to ensure exact results."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "transparent-major",
      "metadata": {
        "id": "transparent-major",
        "outputId": "70766c09-a88f-4179-b6e3-bd60b53e0fd6"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 2.02 s, sys: 853 ms, total: 2.88 s\n",
            "Wall time: 3.14 s\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.neighbors(adata, n_neighbors=15, n_pcs=40)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "photographic-script",
      "metadata": {
        "id": "photographic-script"
      },
      "source": [
        "Next, we calculate the UMAP embedding using RAPIDS.  \n",
        "\n",
        "UMAP (Uniform Manifold Approximation and Projection) is a dimensionality reduction technique that preserves local and global structure in the data.  \n",
        "The RAPIDS implementation accelerates UMAP computation on GPUs, making it significantly faster than the standard CPU version."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "timely-actor",
      "metadata": {
        "id": "timely-actor",
        "outputId": "4f636f39-9f3d-4b0f-db1e-c5155fb77e16"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 448 ms, sys: 190 ms, total: 638 ms\n",
            "Wall time: 1.95 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.umap(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "linear-highlight",
      "metadata": {
        "id": "linear-highlight"
      },
      "source": [
        "### Louvain and Leiden Clustering"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "wooden-submission",
      "metadata": {
        "id": "wooden-submission"
      },
      "source": [
        "Next, we use the **Louvain** and **Leiden** algorithms for graph-based clustering with RAPIDS.  \n",
        "\n",
        "Both methods detect communities (clusters) in the neighborhood graph by optimizing modularity:  \n",
        "- **Louvain**: A hierarchical clustering algorithm that iteratively refines clusters for optimal modularity.  \n",
        "- **Leiden**: An improved version of Louvain that guarantees well-connected, more stable clusters.  \n",
        "\n",
        "Using RAPIDS accelerates the clustering process on GPUs, making it significantly faster than traditional CPU implementations."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "certified-mixer",
      "metadata": {
        "id": "certified-mixer",
        "outputId": "1d906941-816d-4833-8357-759366ce65bb"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 765 ms, sys: 372 ms, total: 1.14 s\n",
            "Wall time: 2.81 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.louvain(adata, resolution=0.6)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "initial-ribbon",
      "metadata": {
        "id": "initial-ribbon",
        "outputId": "876b0d6d-5293-432b-d308-bdc061db869d"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 511 ms, sys: 189 ms, total: 700 ms\n",
            "Wall time: 2.16 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.leiden(adata, resolution=0.6)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "frozen-convertible",
      "metadata": {
        "id": "frozen-convertible",
        "outputId": "7b5053e6-84e8-4c2a-c754-4d9a9350b3dc"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 441068 characters]",
            "text/plain": [
              "<Figure size 1455.6x480 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 1.04 s, sys: 13.9 ms, total: 1.05 s\n",
            "Wall time: 1.05 s\n"
          ]
        }
      ],
      "source": [
        "sc.pl.umap(adata, color=[\"louvain\", \"leiden\"], legend_loc=\"on data\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "amateur-equality",
      "metadata": {
        "id": "amateur-equality",
        "outputId": "573e37c5-4ef8-4f22-de57-7072a2bb84f9"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 542800 characters]",
            "text/plain": [
              "<Figure size 1455.6x480 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig = sc.pl.umap(adata, color=[\"cell_type\", \"assay\"], return_fig=True)\n",
        "ax = fig.axes[0]\n",
        "ax.legend_.set_title(\"cell_type\")\n",
        "ax.legend_.set_bbox_to_anchor((-0.2, -0.4))\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8fbe8181-285f-4ff1-9d1a-28621762b92b",
      "metadata": {
        "id": "8fbe8181-285f-4ff1-9d1a-28621762b92b"
      },
      "source": [
        "### Batch Correction  \n",
        "\n",
        "In the previous UMAP, we observed strong batch effects between assay types.  \n",
        "To correct for this, we apply **Harmony**, a method that aligns different batches while preserving biological variation.  \n",
        "\n",
        "After applying Harmony, we will redo:  \n",
        "- **Neighborhood search** to recompute the k-nearest neighbor graph  \n",
        "- **UMAP** to visualize the corrected embedding  \n",
        "- **Graph-based clustering** to identify cell populations without batch-driven artifacts  \n",
        "\n",
        "This ensures that batch effects do not drive clustering results while retaining meaningful biological structure.\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "351529fa-3ebc-432e-b370-71283a0a45b8",
      "metadata": {
        "id": "351529fa-3ebc-432e-b370-71283a0a45b8",
        "outputId": "ccea6772-13a5-4428-ac5e-91fc106701ae"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 5.15 s, sys: 856 ms, total: 6 s\n",
            "Wall time: 8.85 s\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.harmony_integrate(adata, key=\"assay\", dtype=cp.float32)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "413c0f53-cd12-499d-9bb0-90606e15c6ff",
      "metadata": {
        "id": "413c0f53-cd12-499d-9bb0-90606e15c6ff",
        "outputId": "b19512b4-6680-4cea-bf6c-5bdaccc1c354"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 1.96 s, sys: 1.16 s, total: 3.12 s\n",
            "Wall time: 7.84 s\n"
          ]
        }
      ],
      "source": [
        "rsc.pp.neighbors(adata, n_neighbors=15, n_pcs=40,use_rep=\"X_pca_harmony\",key_added=\"harmony\")\n",
        "rsc.tl.umap(adata,neighbors_key=\"harmony\",key_added=\"X_umap_harmony\")\n",
        "rsc.tl.louvain(adata, resolution=0.6,neighbors_key=\"harmony\",key_added=\"louvain_harmony\")\n",
        "rsc.tl.leiden(adata, resolution=0.6,neighbors_key=\"harmony\",key_added=\"leiden_harmony\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e07cbbbb-cc5c-4e1c-9295-863f062f96cf",
      "metadata": {
        "id": "e07cbbbb-cc5c-4e1c-9295-863f062f96cf",
        "outputId": "cd566a3b-ce3c-45d9-d4d1-fd5bc8022a9d"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 537824 characters]",
            "text/plain": [
              "<Figure size 1455.6x480 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 1.07 s, sys: 5.37 ms, total: 1.07 s\n",
            "Wall time: 1.07 s\n"
          ]
        }
      ],
      "source": [
        "sc.pl.embedding(adata,basis=\"X_umap_harmony\", color=[\"louvain_harmony\", \"leiden_harmony\"], legend_loc=\"on data\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "85fc1084-ce41-4f82-a1a0-661af80bccee",
      "metadata": {
        "id": "85fc1084-ce41-4f82-a1a0-661af80bccee",
        "outputId": "8cf67f0b-e580-469b-f593-b0d0e99b5063"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 648444 characters]",
            "text/plain": [
              "<Figure size 1455.6x480 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig = sc.pl.embedding(adata,basis=\"X_umap_harmony\", color=[\"cell_type\", \"assay\"], return_fig=True)\n",
        "ax = fig.axes[0]\n",
        "ax.legend_.set_title(\"cell_type\")\n",
        "ax.legend_.set_bbox_to_anchor((-0.2, -0.4))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "boring-bulgarian",
      "metadata": {
        "id": "boring-bulgarian"
      },
      "source": [
        "### t-SNE"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "appreciated-designer",
      "metadata": {
        "id": "appreciated-designer"
      },
      "source": [
        "Next, we use **t-distributed Stochastic Neighbor Embedding (t-SNE)** to visualize cells in two dimensions.  \n",
        "t-SNE is a non-linear dimensionality reduction technique that preserves local structure and reveals complex patterns in the data.  \n",
        "\n",
        "We leverage the RAPIDS GPU implementation of t-SNE, which is significantly faster than CPU-based methods.  \n",
        "This allows us to efficiently process large datasets while maintaining high-quality embeddings.  \n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "touched-hollywood",
      "metadata": {
        "id": "touched-hollywood",
        "outputId": "63763e32-240a-4f33-8344-d5916a4d140f"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[2025-12-04 23:31:40.093] [CUML] [warning] # of Nearest Neighbors should be at least 3 * perplexity. Your results might be a bit strange...\n",
            "CPU times: user 3.1 s, sys: 4.84 s, total: 7.95 s\n",
            "Wall time: 11.5 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.tsne(adata, n_pcs=40, perplexity=30, early_exaggeration=12, learning_rate=200, use_rep=\"X_pca_harmony\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "statutory-supplement",
      "metadata": {
        "id": "statutory-supplement",
        "outputId": "2768907a-7338-405e-ee1e-fdd8d59e76af"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 662912 characters]",
            "text/plain": [
              "<Figure size 1455.6x480 with 2 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig = sc.pl.tsne(adata, color=[\"cell_type\", \"assay\"], return_fig=True)\n",
        "ax = fig.axes[0]\n",
        "ax.legend_.set_title(\"cell_type\")\n",
        "ax.legend_.set_bbox_to_anchor((-0.2, -0.4))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "sexual-delaware",
      "metadata": {
        "id": "sexual-delaware"
      },
      "source": [
        "### Differential expression analysis"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fallen-exposure",
      "metadata": {
        "id": "fallen-exposure"
      },
      "source": [
        "To identify key marker genes for each cell type, we use **logistic regression** to compute a ranking of highly differential genes.  \n",
        "Unlike traditional statistical tests, logistic regression models the probability of a gene being highly expressed in a given cluster  \n",
        "while accounting for all genes simultaneously, making it more robust for complex datasets.  \n",
        "\n",
        "We rank the top 50 genes that best distinguish each **cell type**, helping to characterize biological differences between clusters.  "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "naked-treasury",
      "metadata": {
        "id": "naked-treasury",
        "outputId": "8d45580c-3576-45cb-8e3f-5bbaeb072852"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 16.4 s, sys: 14.9 s, total: 31.3 s\n",
            "Wall time: 49.3 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.rank_genes_groups_logreg(adata, groupby=\"cell_type\", use_raw=False)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "082233ed-f984-40ee-a000-f8354105f542",
      "metadata": {
        "id": "082233ed-f984-40ee-a000-f8354105f542",
        "outputId": "704e5712-bf9e-47eb-d109-6fd522ebd6d6"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 505768 characters]",
            "text/plain": [
              "<Figure size 2560x1920 with 16 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sc.pl.rank_genes_groups(adata)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "verbal-programming",
      "metadata": {
        "id": "verbal-programming"
      },
      "source": [
        "### Diffusion Map Embedding  \n",
        "\n",
        "Next, we compute **Diffusion Maps**, a nonlinear dimensionality reduction method that preserves the continuous structure of the data.  \n",
        "Unlike UMAP and t-SNE, Diffusion Maps are well-suited for capturing **trajectory-like** relationships in single-cell data.  \n",
        "\n",
        "We use the batch-corrected neighborhood graph (`neighbors_key=\"harmony\"`) to ensure that the embedding is not influenced by batch effects.  "
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "controlling-portugal",
      "metadata": {
        "id": "controlling-portugal",
        "outputId": "d54f26df-5046-44d4-9e21-cbba6ac9a771"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "CPU times: user 2.4 s, sys: 165 ms, total: 2.57 s\n",
            "Wall time: 3.35 s\n"
          ]
        }
      ],
      "source": [
        "rsc.tl.diffmap(adata,neighbors_key=\"harmony\")\n",
        "#Due to an issue with plotting in Scanpy, we need to shift the first component of the diffusion map by removing the first column:\n",
        "adata.obsm[\"X_diffmap\"] = adata.obsm[\"X_diffmap\"][:, 1:]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "affiliated-excess",
      "metadata": {
        "id": "affiliated-excess",
        "outputId": "d1dff18c-742a-4b09-f424-341f1987ade5"
      },
      "outputs": [
        {
          "data": {
            "image/png": "[encoded data, 203708 characters]",
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "sc.pl.diffmap(adata, color=\"cell_type\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "77vDiuAjvYtP",
      "metadata": {
        "id": "77vDiuAjvYtP"
      },
      "source": [
        "Let's store this final output, which will be needed for the next notebooks in this series."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "23354b8e-009b-481c-8d9d-7e9736bdb121",
      "metadata": {
        "id": "23354b8e-009b-481c-8d9d-7e9736bdb121"
      },
      "outputs": [],
      "source": [
        "adata.write(\"h5/dli_decoupler.h5ad\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "viVb3vxT5fxb",
      "metadata": {
        "id": "viVb3vxT5fxb"
      },
      "source": [
        "## Conclusion"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "AHf-fArS5iAW",
      "metadata": {
        "id": "AHf-fArS5iAW"
      },
      "source": [
        "And with that, we have completed the entire end-to-end workflow for single-cell analysis. From data loading and filtering to clustering and visualization. Thanks to the speed of RAPIDS-singlecell we were able to run nearly every cell in real time, allowing us to iterate faster and tackle larger and larger datasets."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "59987984-5ca7-45a8-9369-ec8fabb39d65",
      "metadata": {
        "id": "59987984-5ca7-45a8-9369-ec8fabb39d65"
      },
      "source": [
        "## Next Steps\n",
        "\n",
        "We can now take this output file, `dli_decoupler.h5ad`, and use it further analysis, like **[Transcriptional Regulatory Analysis](https://github.com/NVIDIA-AI-Blueprints/single-cell-analysis-blueprint/blob/main/notebooks/02_scRNA_analysis_extended.ipynb)**.  You can quickly download the notebook and continue your journey by uncommenting and running the command below:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e1c5c0cb-4110-43c9-beba-131ddb9cf895",
      "metadata": {
        "id": "e1c5c0cb-4110-43c9-beba-131ddb9cf895"
      },
      "outputs": [],
      "source": [
        "# !wget https://raw.githubusercontent.com/NVIDIA-AI-Blueprints/single-cell-analysis-blueprint/refs/heads/main/notebooks/02_scRNA_analysis_extended.ipynb"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.13.7"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
