Human and Mammalian Brain Atlas (HMBA) - Marmoset Subcortex Xenium Spatial Transcriptomics: Gene Expression#

The Marmoset Subcortical Cell Atlas (MSCA), is a multiomic atlas of the common marmoset (Callithrix jacchus) subcortex that integrates single nucleus transcriptomics, chromatin accessibility, and spatial transcriptomics. Part of the Human and Mammalian Brain Atlas (HMBA) effort within the NIH’s BRAIN Initiative® Cell Atlas Network (BICAN), this dataset comprehensively samples key subcortical structures in the non-human primate, including the hippocampal formation, amygdala, olfactory areas, basal forebrain, thalamus, hypothalamus, midbrain, and major hindbrain structures (excluding cerebellar cortex).

This cellular-resolution Xenium spatial transcriptomic dataset includes 5.6 million cells profiled from 47 coronal hemisections from a single adult male marmoset donor, sampling the rostral-caudal extent of subcortical structures. The spatial cells were mapped to the 1,737 snRNA-seq Clusters defined in the MSCA reference cell type taxonomy, and the resulting spatial localization of the Clusters provided anatomical anchoring and nomenclature to the Group and Class hierarchy levels of the MSCA taxonomy.

For more information on this dataset and the associated 10X Multiome data, please refer to the publication: Dan, Turner, DeBerardine, et al. (2026). Spatial and regulatory organization of cell identity across the primate subcortex. Cell. https://doi.org/10.1016/j.cell.2026.10.007.


In this notebook, we build on the our previous investigation into the cell level metadata, combining the metadata gene expressions for select genes that we extract from the released h5ad files.

You need to be connected to the internet to run this notebook and have run through the getting started notebook.

import json
import geojson
from typing import List, Tuple, Optional

import pandas as pd
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

from abc_atlas_access.abc_atlas_cache.abc_project_cache import AbcProjectCache
from abc_atlas_access.abc_atlas_cache.anndata_utils import get_gene_data

We will interact with the data using the AbcProjectCache. This cache object tracks which data has been downloaded and serves the path to the requested data on disk. For metadata, the cache can also directly serve a up a Pandas DataFrame. See the getting_started notebook for more details on using the cache including installing the cache if it has not already been.

Change the download_base variable to where you would like to download the data in your system.

download_base = Path('../../data/abc_atlas')
abc_cache = AbcProjectCache.from_cache_dir(
    cache_dir=download_base,
)

abc_cache.current_manifest
/Users/chris.morrison/src/abc_atlas_access/src/abc_atlas_access/abc_atlas_cache/cloud_cache.py:665: UserWarning: The manifest version recorded as last used for this cache -- releases/20261130/manifest.json-- is not a valid manifest for this dataset. Loading latest version -- releases/20261101/manifest.json -- instead.
  warnings.warn(msg, UserWarning)
/Users/chris.morrison/src/abc_atlas_access/src/abc_atlas_access/abc_atlas_cache/cloud_cache.py:697: OutdatedManifestWarning: You are loading
releases/20261101/manifest.json
which is newer than the most recent manifest file you have previously been working with
releases/20261130/manifest.json
It is possible that some data files have changed between these two data releases, which will force you to re-download those data files (currently downloaded files will not be overwritten). To continue using releases/20261130/manifest.json, run
type.load_manifest('releases/20261130/manifest.json')
  warnings.warn(msg, OutdatedManifestWarning)
'releases/20261130/manifest.json'

Cell metadata#

The Marmoset Subcortext spatial dataset has it’s own h5ad files and associated cell metadata tables. These metadata contain the basic identifiers for each cell and forms the basis by which we will join all other cell level and associated data. In the previous tutorial, we merged the cell metadata tables with their associated specimen metadatadata and then with the rotated coordinates we use to plot the cell locations.

Below we load the base metadata needed for extracting gene expressions.

# Load the cell metadata and specimen metadata
# and join them together with the rotated coordinates.

cell_metadata = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='cell_metadata',
    dtype={"cell_label": str},
).set_index('cell_label')

specimen = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='specimen_metadata',
    dtype={"cell_label": str}
).set_index('brain_section_label')

cell_metadata = cell_metadata.join(specimen, on='brain_section_label', rsuffix='_specimen')

rotated_coordinates = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='rotated_coordinates',
    dtype={"cell_label": str}
).set_index('cell_label')
cell_metadata = cell_metadata.join(
    rotated_coordinates[['x_rotated_mm', 'y_rotated_mm']],
    on='cell_label'
)

del specimen
del rotated_coordinates

cell_metadata.head()
brain_section_label brain_section_barcode x_experiment y_experiment segmentation_job_id segmentation_method cell_area nucleus_area n_nuclei_in_cell total_counts ... hemisphere brain_division_label brain_division_ordinal donor_label slab_plane_ordinal slab_plane_label slab_plane_order slab_z_order x_rotated_mm y_rotated_mm
cell_label
1382042951_aaondhbe-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4459.1006 4524.2420 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 89.81579 60.735157 1 339 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.651601 -5.898891
1382042951_aaoneabm-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4668.6323 4653.2130 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 105.98172 74.327190 1 762 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.508436 -6.098991
1382042951_aaonekin-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4593.0610 4594.6304 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.50813 74.598130 1 406 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.555394 -6.015698
1382042951_aaoofhco-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4529.3900 4348.2266 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 136.37189 72.475784 1 948 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.518584 -5.763877
1382042951_aaopgffc-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4524.6533 4382.6323 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.41782 78.391250 1 941 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.536286 -5.793757

5 rows × 40 columns

Gene expression data#

Now we can extract expression for specific genes from the h5ad files and pair it with our gene metadata. In general, we can use the function get_gene_data to extract the expression of specific genes for all the cells in a given dataset or a subset of cells.. For more details on using this convenience function, see the Accessing 10x RNA-seq gene expression data tutorial notebook.

For this tutorial we will use precomputed tables of the expression for specific genes to compare expression both spatially and by the taxonomy.

To use the gene expression data, we first load the set of genes for the subcortex data.

genes = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='gene',
).set_index('gene_identifier')
genes.head()
gene.csv: 100%|██████████| 22.4k/22.4k [00:00<00:00, 172kMB/s] 
gene_symbol description molecular_type
gene_identifier
NCBIGene:100393956 ABI3BP ABI family member 3 binding protein protein_coding
NCBIGene:100388523 ABL2 ABL proto-oncogene 2, non-receptor tyrosine ki... protein_coding
NCBIGene:100389219 ABLIM1 actin binding LIM protein 1 protein_coding
NCBIGene:100400228 ACAP2 ArfGAP with coiled-coil, ankyrin repeat and PH... protein_coding
NCBIGene:100392257 ACHE acetylcholinesterase (Cartwright blood group) protein_coding

We define a set of genes to load via their symbol. We reuse the smaller overlapping subset of genes we used in the previous 10X gene expression notebook.

gene_names = ['AQP4', 'DRD1', 'DRD2', 'SLC17A6']

In the cell below, we show how to use the get_gene_data function to extract the expression of specific genes from the h5ad metadata. In the commented code, we combine our abc_cache object, cell metadata, gene metadata, and gene symbols we would like select in our get_gene_data function that downloads and extracts gene expression data from the h5ad file. Note that this function is an example and should not be used on small systems to extract a large number of genes for all cells at once. For a small subset of cells, it can be used to pull expression data from the h5ad file for all genes.

The methods below will pull data from the natural-log, batch-normalized data. These are data where the raw counts are normalized by scaling all cells within each section (per brain_section_barcode) to the mean total counts of a reference cell population, in this case the counts in the cell type Group, Astrocyte. This is done because their population count correlates with the total neuron count across sections and they are distributed across diverse anatomical regions using the following qsudo-code:

section_factor = global_reference_cells_mean / section_reference_cells_mean
batch_normalized_counts = section_factor * raw_counts

For this tutorial we will skip using these functions and instead use a pre-computed gene expression to save time. For specifics on how to use the below functions see this tutorial.

"""
gene_data = get_gene_data(
    abc_atlas_cache=abc_cache,
    all_cells=cell_metadata,
    all_genes=genes,
    selected_genes=gene_names,
    data_type='ln'
)"""
"\ngene_data = get_gene_data(\n    abc_atlas_cache=abc_cache,\n    all_cells=cell_metadata,\n    all_genes=genes,\n    selected_genes=gene_names,\n    data_type='ln'\n)"

We load the precomputed gene expressions for each species below instead of running the processing.

gene_data = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='example_gene_expression',
).set_index('cell_label')
gene_data.head()
example_gene_expression.csv: 100%|██████████| 291M/291M [00:43<00:00, 6.64MMB/s]   
AQP4 DRD1 DRD2 SLC17A6
cell_label
1382042951_aaondhbe-1-20250227 0.000000 0.926574 0.000000 0.0
1382042951_aaoneabm-1-20250227 0.000000 0.000000 0.566971 0.0
1382042951_aaonekin-1-20250227 0.566971 0.566971 0.000000 0.0
1382042951_aaoofhco-1-20250227 0.000000 0.566971 0.000000 0.0
1382042951_aaopgffc-1-20250227 0.000000 0.000000 0.000000 0.0

Now that we’ve loaded our expression matrix tables, indexed by cell_label, we can merge them into our cell metadata tables.

cell_metadata_with_genes = cell_metadata.join(gene_data, on='cell_label')

# clean up memory
del gene_data

Geojson polygons#

The dataset includes polygons outlining the hemisphere boundary of each brain section, which can be plotted over rotated coordinates to allow us to orient the subcortical cells in their full hemisection contex. These polygons are provided in geojson format, and here we use the associated geojson python package to load the jsons for use in our visualization.

Additional, manually drawn polygons outlining select regions of the Basal Ganglia (STR, GP, GPe, GPi, VeP, STH, and SN), as well as polygons identifying the cells that are also part of the HMBA-BG dataset (basal_ganglia_and_adjacent), are included as part of the HMBA-BG-Xenium-Marmoset dataset and are described and can be accessed in that dataset’s equivalent tutorial notebook.

First, we load the term set for these data. All cells in this HMBA-Subcortex-Xenium-Marmoset dataset receive the term set label “Subcortical”.

value_sets = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='value_sets'
).set_index('label')
pred = value_sets['field'] == 'parcellation_term_symbol'
parcellation_terms = value_sets[pred].copy()
parcellation_terms.head()
table field description order external_identifier parent_label color_hex_triplet
label
Hemisphere cell_anatomical_annotations parcellation_term_symbol Brain Hemisphere 1 hemisphere NaN #000000
Subcortical cell_anatomical_annotations parcellation_term_symbol Subcortical regions 2 subcortical Hemisphere #008080
Basal ganglia and adjacent cell_anatomical_annotations parcellation_term_symbol Basal ganglia and adjacent 3 basal_ganglia_and_adjacent Subcortical #BEBEBE
STR cell_anatomical_annotations parcellation_term_symbol striatum 4 HOMBA:10333 Basal ganglia and adjacent #cd86cc
GP cell_anatomical_annotations parcellation_term_symbol globus pallidus 5 HOMBA:10342 Basal ganglia and adjacent #7AE361

Below we define two functions that will be used in our plotting of the geojsons. The first takes in the data and a matplotlib axis object and plots the geojson data on the provided axis, extracting color and name information from the value/term set. The second loads the data based on the appropriate slab and species provided.

def plot_geo_data(geo_data: dict, ax: plt.Axes, terms=parcellation_terms):
    """Plot a geojson polygon over the input axis.

    Parameters
    ----------
    geo_data: dict
        Geojson formatted dictionary containing Polygons to plot.
    ax: matplotlib.pyplot.axis
        Axis object to plot the geojson data over.
    terms: pandas.DataFrame
        Term list containing the colors and orders for the parcellation, brain region terms. 
    """
    if geo_data is None:
        return None
    # If the geo_data is a FeatureCollection, we need to iterate over its
    # features and plot each one.
    if geo_data['type'] == 'FeatureCollection':
        for sub_data in geo_data['features']:
            plot_geo_data(sub_data, ax)
    elif geo_data['type'] == 'Feature':
        geometry_data = geo_data['geometry']
        name = geo_data['properties']['label']
        # color = terms.loc[name, 'color_hex_triplet'] # Get the color for this object from the term list.
        color = 'black' # Default color for the polygons.
        # If the geometry is a GeometryCollection, we need to iterate over its
        # geometries that make up the complete feature.
        if geometry_data['type'] == 'GeometryCollection':
            for idx, geometry in enumerate(geometry_data['geometries']):
                coords = np.array(list(geojson.coords(geometry)))
                # Only label the first geometry in the collection. Others
                # will be plotted without a label to avoid duplicate legend entries.
                if idx == 0:
                    ax.fill(coords[:, 0],
                            coords[:, 1],
                            edgecolor=color,
                            fill=False,
                            linewidth=3,
                            label=name)
                else:
                    ax.fill(coords[:, 0],
                            coords[:, 1],
                            edgecolor=color,
                            fill=False,
                            linewidth=3)
        else:
            coords = np.array(list(geojson.coords(geometry_data)))
            try:
                ax.fill(coords[:, 0],
                        coords[:, 1],
                        edgecolor=color,
                        fill=False,
                        linewidth=3,
                        label=name)
            except ValueError as e:
                import pdb; pdb.set_trace()
    else:
        print("unknown data", geo_data)

def get_polygons(slab_name: str) -> dict:
    """Load the geojson polygons for the specified slab and species.

    Parameters
    ----------
    slab_name: str
        Name of the slab to load.

    Returns
    -------
    dict
        Geojson formatted dictionary containing the polygons for the specified slab and species.
    """

    base_dir = 'HMBA-Subcortex-Xenium-Marmoset'

    file_path = abc_cache.get_file_path(
        directory=base_dir,
        file_name=slab_name,
    )
    if not file_path.exists():
        return None

    with open(file_path) as jfile:
        data = json.load(jfile)
    
    return data

Plotting the Slabs#

We define a helper function plot_slabs to visualize the cells for a specified set of brain sections either by colorized metadata or gene expression. This function can be used to plot individual brain sections as well.

def plot_slabs(
    df: pd.DataFrame,
    feature: str,
    slab_list: List[str],
    cmap: Optional[plt.Colormap] = None,
    fig_width: float = 40,
    fig_height: float = 8,
    plot_polygons: bool = False,
    point_size: float = 1.0,
    fix_bounds: bool = False
) -> Tuple[plt.Figure, np.ndarray]:
    """Plot spatial transcriptomics data.

    Parameters
    ----------
    df: pandas.DataFrame
        cell metadata data frame with x,y locations to plot.
    feature: str
        Expression value or color column to plot.
    slab_list: List[str]
        List of slab_plane ids to plot.
    cmap: matplotlib.colors.Colormap
        Colormap for expression features.
    fig_width: int
        Width of the plotted figure.
    fig_height: int
        Height of the plotted figure.
    plot_polygons: bool
        Overplot geojson polygons. Note that these are note available for human
        and this should be set to False.
    point_size: float
        Size of the points to plot.
    fix_bounds: bool
        If True, fix the x and y bounds across all slabs to be the same.
    """
    if isinstance(slab_list, str):
        slab_list = [slab_list]

    fig, ax = plt.subplots(1, len(slab_list))
    fig.set_size_inches(fig_width, fig_height)

    if len(slab_list) == 1:
        ax = [ax]

    if 'x' in df.columns:
        x_col = 'x'
    elif 'x_rotated_mm' in df.columns:
        x_col = 'x_rotated_mm'
    if 'y' in df.columns:
        y_col = 'y'
    elif 'y_rotated_mm' in df.columns:
        y_col = 'y_rotated_mm'

    if fix_bounds:
        x_min = df[x_col].min() * 0.8
        x_max = df[x_col].max() * 1.2
        y_min = df[y_col].min() * 0.8
        y_max = df[y_col].max() * 1.2

    for idx, slab in enumerate(slab_list):

        if 'slab_plane_label' in df.columns:
            filtered = df[df['slab_plane_label'] == slab]
        elif 'brain_section_label' in df.columns:
            filtered = df[df['brain_section_label'] == slab]
        
        xx = filtered[x_col]
        yy = filtered[y_col]
        vv = filtered[feature]
        
        if cmap is not None:
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker='.', cmap=cmap)
        else:
            
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker=".")

        if plot_polygons:
            plot_geo_data(get_polygons(slab), ax[idx])
            
        ax[idx].axis('equal')
        if fix_bounds:
            ax[idx].set_ylim(y_min, y_max)
            ax[idx].set_xlim(x_min, x_max)
        ylim = ax[idx].get_ylim()
        ax[idx].set_xticks([])
        ax[idx].set_yticks([])
        
        ax[idx].set_title(slab)
        ax[idx].legend(loc=0)
        
    plt.subplots_adjust(wspace=0.01, hspace=0.01)
    return fig, ax

Below we plot the spatial expression of our genes across the different species. We select a minium expression value of which effectively masks out zero expression cells from the plots, showing a bit more contrast. Feel free to experiment with different vmin and vmax values in investigate the gene expressions more in depth.

gene_names = cell_metadata_with_genes.columns[
    cell_metadata_with_genes.columns.isin(gene_names)
].to_numpy()
slab_list = [
    'CJ23.56.004.CX.42.Plane.05', 'CJ23.56.004.CX.42.Plane.13', 'CJ23.56.004.CX.43.Plane.05',
    'CJ23.56.004.CX.43.Plane.15', 'CJ23.56.004.CX.44.Plane.03'
]
for gene_symbol in gene_names:
    fig, ax = plot_slabs(
        cell_metadata_with_genes,
        gene_symbol,
        slab_list,
        cmap=plt.cm.magma_r,
        plot_polygons=True,
    )
    fig.suptitle(f'Gene Expression: {gene_symbol}')
    plt.show()
../_images/9c93bbd44d7514ceaf612011b2aa79a489749816016ed372cd6a214b15f53137.png ../_images/22c5d6bca43671716c40ed74860d285bf935214a9b848972db5c1a8240a5b738.png ../_images/98683393361e37d74a5003511342199dce6a2ea6732a7f41859b960b5486c23a.png ../_images/ae70469d61dd932dcbff72cae0957d58278d98572af16deea9329a50400147a6.png

Taxonomy mapping#

Finally, let’s load the taxonomy information so that we can plot gene expression versus various taxons in a heatmap for each species. Below, we load and merge all the information we need to plot cell colors and taxonomy as done in previous HMBA-Subcortex tutorial.

Note that the taxonomy mapping also contains neurotransmitter and dominant brain region locations.

Below we load the subcortex taxonomy. For more details on this taxonomy see the notebook here.

cell_to_cluster_membership = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-Xenium-Marmoset', 'cell_to_cluster_membership'
).set_index('cell_label')

cluster = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-taxonomy-Marmoset', 'cluster'
).set_index('label')
cluster_annotation_term = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-taxonomy-Marmoset', 'cluster_annotation_term'
).set_index('label')

cluster_annotation_term_set = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_annotation_term_set'
).set_index('label')

cluster_to_cluster_annotation_membership = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_cluster_annotation_membership'
).set_index('cluster_annotation_term_label')

membership_with_cluster_info = cluster_to_cluster_annotation_membership.join(
    cluster.reset_index().set_index('cluster_alias')[['number_of_cells']],
    on='cluster_alias'
)

membership_with_cluster_info = membership_with_cluster_info.join(cluster_annotation_term, rsuffix='_anno_term').reset_index()

membership_groupby = membership_with_cluster_info.groupby(
    ['cluster_alias', 'cluster_annotation_term_set_name']
)

cluster_details = membership_groupby['cluster_annotation_term_name'].first().unstack()
cluster_order = membership_groupby['term_order'].first().unstack()

cluster_colors = membership_groupby['color_hex_triplet'].first().unstack()
cluster_colors = cluster_colors[cluster_annotation_term_set['name']]

cluster_to_dominant_region = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_dominant_region',
).set_index('cluster_alias')
cluster_to_neurotransmitter = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_neurotransmitter',
).set_index('cluster_alias')

cell_metadata_with_genes = cell_metadata_with_genes.join(cell_to_cluster_membership)
cell_metadata_with_genes = cell_metadata_with_genes.join(cluster_details, on='cluster_alias')
cell_metadata_with_genes = cell_metadata_with_genes.join(cluster_colors, on='cluster_alias', rsuffix='_color')
cell_metadata_with_genes = cell_metadata_with_genes.join(cluster_order, on='cluster_alias', rsuffix='_order')
cell_metadata_with_genes = cell_metadata_with_genes.join(cluster_to_dominant_region, on='cluster_alias', rsuffix='_region')
cell_metadata_with_genes = cell_metadata_with_genes.join(cluster_to_neurotransmitter, on='cluster_alias', rsuffix='_neurotransmitter')

del cell_to_cluster_membership

cell_metadata.head()
brain_section_label brain_section_barcode x_experiment y_experiment segmentation_job_id segmentation_method cell_area nucleus_area n_nuclei_in_cell total_counts ... hemisphere brain_division_label brain_division_ordinal donor_label slab_plane_ordinal slab_plane_label slab_plane_order slab_z_order x_rotated_mm y_rotated_mm
cell_label
1382042951_aaondhbe-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4459.1006 4524.2420 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 89.81579 60.735157 1 339 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.651601 -5.898891
1382042951_aaoneabm-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4668.6323 4653.2130 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 105.98172 74.327190 1 762 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.508436 -6.098991
1382042951_aaonekin-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4593.0610 4594.6304 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.50813 74.598130 1 406 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.555394 -6.015698
1382042951_aaoofhco-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4529.3900 4348.2266 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 136.37189 72.475784 1 948 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.518584 -5.763877
1382042951_aaopgffc-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4524.6533 4382.6323 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.41782 78.391250 1 941 ... right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1 3.536286 -5.793757

5 rows × 40 columns

def extract_value_set(
    cell_metadata_df: pd.DataFrame,
    input_value_set: pd.DataFrame,
    input_value_set_label: str,
    value_set_label: str=None
):
    """Add color and order columns to the cell metadata dataframe based on the input
    value set.

    Columns are added as {input_value_set_label}_color and {input_value_set_label}_order.

    Parameters
    ----------
    cell_metadata_df : pd.DataFrame
        DataFrame containing cell metadata.
    input_value_set : pd.DataFrame
        DataFrame containing the value set information.
    input_value_set_label : str
        The the column name to extract color and order information for. will be added to the cell metadata.
    """
    if value_set_label is None:
        value_set_label = input_value_set_label
    cell_metadata_df[f'{input_value_set_label}_color'] = input_value_set[
        input_value_set['field'] == value_set_label
    ].loc[cell_metadata_df[input_value_set_label]]['color_hex_triplet'].values
    cell_metadata_df[f'{input_value_set_label}_order'] = input_value_set[
        input_value_set['field'] == value_set_label
    ].loc[cell_metadata_df[input_value_set_label]]['order'].values

extract_value_set(cell_metadata_with_genes, value_sets, 'Cluster_neurotransmitter', 'neurotransmitter')
extract_value_set(cell_metadata_with_genes, value_sets, 'Group_neurotransmitter', 'neurotransmitter')
extract_value_set(cell_metadata_with_genes, value_sets, 'Cluster_dominant_region', 'dominant_region')
extract_value_set(cell_metadata_with_genes, value_sets, 'Group_dominant_region', 'dominant_region')

Plotting heatmap expression data.#

We define a helper functions aggregate_by_metadata to compute the average expression for a given category. Note that this is a simple wrapper of the Pandas GroupBy functionality. Other summary statistics beyond just the mean are listed here: https://pandas.pydata.org/docs/reference/groupby.html#dataframegroupby-computations-descriptive-stats

We also define a function to plot the resultant averaged data in a heatmap.

def aggregate_by_metadata(df: pd.DataFrame, gnames: List[str], value: str, sort: bool = False) -> pd.DataFrame:
    """Aggregate gene expression data by metadata.

    Parameters
    ----------
    df: pandas.DataFrame
        DataFrame containing gene expression and metadata.
    gnames: List[str]
        List of gene names to aggregate.
    value: str
    sort: bool
        Metadata column to group by.
        If True, sort the output by the first gene in gnames.

    Returns
    -------
    pandas.DataFrame
        DataFrame with aggregated gene expression values.
    """
    grouped = df.groupby(value)[gnames].mean()
    if sort:
        grouped = grouped.sort_values(by=gnames[0], ascending=False)
    return grouped


def plot_heatmap(
        df: pd.DataFrame,
        fig_width: int = 8,
        fig_height: int = 4,
        cmap: mpl.colormaps = plt.cm.magma,
        vmax: Optional[float] = None,
        unit: str = '[ln(batch-normalized + 1)]'
    ) -> Tuple[plt.Figure, plt.Axes]:
    """Plot a heatmap from a DataFrame.

    Parameters
    ----------
    df: pandas.DataFrame
        DataFrame to plot as a heatmap.
    fig_width: int
        Width of the figure.
    fig_height: int
        Height of the figure.
    cmap: matplotlib.colors.Colormap
        Colormap to use for the heatmap.

    Returns
    -------
    fig: matplotlib.pyplot.Figure
        Figure object containing the heatmap.
    ax: matplotlib.pyplot.Axes
        Axes object containing the heatmap.
    """

    arr = df.to_numpy().astype('float')

    vmin = arr.min()
    vmax = arr.max()
    norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
    sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
    cmap = sm.get_cmap()

    fig, ax = plt.subplots()
    fig.set_size_inches(fig_width, fig_height)

    ax.imshow(arr, cmap=cmap, aspect='auto')
    xlabs = df.columns.values
    ylabs = df.index.values

    ax.set_xticks(range(len(xlabs)))
    ax.set_xticklabels(xlabs)

    ax.set_yticks(range(len(ylabs)))
    ax.set_yticklabels(ylabs)

    cbar = fig.colorbar(sm, ax=ax, orientation='vertical', fraction=0.1, pad=0.01)
    cbar.set_label(unit)

    return fig, ax

Visualize gene expression for selected genes by the HMBA-Subcortex taxonomy.#

Below we average the expression of our selected genes across taxons from the HMBA-Subcortex taxonomy. We show the expression for taxons at the class level for each species. For a side by side comparison of gene expression heatmaps for each species, see the HMBA 10X gene expression tutorial.

gene_names = cell_metadata_with_genes.columns[
    cell_metadata_with_genes.columns.isin(gene_names)
].to_numpy()
agg = aggregate_by_metadata(cell_metadata_with_genes, gene_names, 'Class', sort=True)
plot_heatmap(agg, 8, 5)
plt.title('Class')
plt.show()
../_images/d96e864b5973f506c9dd16f7f21973e96b43174b7995c302d4908d7be239a6be.png

Visualize gene expression for selected genes by dominant region.#

Below we average the expression of our selected genes across the different dominant regions that each cell is uniquely assigned to. We compute these expressions for each species individually.

gene_names = cell_metadata_with_genes.columns[
    cell_metadata_with_genes.columns.isin(gene_names)
].to_numpy()
for level in ['Group', 'Cluster']:
    agg = aggregate_by_metadata(cell_metadata_with_genes, gene_names, f'{level}_dominant_region', sort=True)
    plot_heatmap(agg, 8, 5)
    plt.title(f'{level} Dominant Region')
    plt.show()
../_images/3f69eecb78870214832e4462614859d6e0ebb7c5d28b65b394e775de9e09a580.png ../_images/2cba84bee8e7ec5697f8c73c6f249dab77cfbdab0afdea55f842ae121fbace8a.png

Visualize gene expression for selected genes by the HMBA-Subcortex taxonomy.#

Below we average the expression of our selected genes across the neurotransmitter at the Group and Cluster level.

gene_names = cell_metadata_with_genes.columns[
    cell_metadata_with_genes.columns.isin(gene_names)
].to_numpy()
for level in ['Group', 'Cluster']:
    agg = aggregate_by_metadata(cell_metadata_with_genes, gene_names, f'{level}_neurotransmitter', sort=True)
    plot_heatmap(agg, 8, 5)
    plt.title(f'{level} Neurotransmitter')
    plt.show()
../_images/ddbdc4cccad327ef240ec31fa44e8d9accd69b810b6d2291ee4599e61f2f6c2c.png ../_images/1a27c002d42c820c475469071a9000a6917ef4a1b095cf31e96404718e2d6ec6.png

To learn more about the spatial data see the HBMA-Subcortex Spatial Slabs and Taxonomy notebook.

To learn more about the taxonomy used in this notebook and the data it is derived from, see the HMBA-Subcortex Clustering Analysis and Taxonomy notebook.