Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Build a neighborhood graph stage by stage

A cell graph describes which cells resemble one another. Each node is a cell, and an edge connects it to a nearby cell in the chosen representation. CyteArc searches for k nearest neighbors per cell, then converts their distances into weighted connections used by clustering, embeddings, imputation, and trajectory methods.

For RNA, similarity is usually measured in PCA space; for ATAC, it is measured in LSI space. Reducing dimensionality limits the influence of noisy features and makes neighbor search more efficient. Nearby cells share patterns in the selected representation, but this does not by itself establish a common cell identity. Batch effects and other unwanted variation can also bring cells together.

CyteArc first builds an approximate neighbor index, then queries neighbors and calculates connectivity. Calling these stages directly helps when you need to change one parameter, insert batch correction, or reuse an expensive reduction. Start with Identify PBMC populations with scRNA-seq if you have not yet run an analysis. This page explains the individual steps behind that workflow.

Feature selection is covered in Feature selection: choosing informative features. This guide begins once a feature-selection artifact exists.

The standard graph workflow

For RNA, the graph stages are:

  1. Normalize the selected genes.

  2. Reduce them with PCA.

  3. Optionally correct the reduced coordinates with Harmony.

  4. Build an approximate-neighbour index.

  5. Query k neighbours per cell.

  6. Convert neighbour distances into weighted connectivity.

  7. Build a separate initialization for UMAP and t-SNE.

ATAC follows the same shape but uses TF-IDF normalization and LSI. The initialization does not define graph edges; it only supplies starting coordinates for a layout.

ds.pipeline.run() orchestrates the standard RNA path and can continue through UMAP, clustering, doublet scoring, and markers. Use it when the defaults match the analysis. The stage methods below expose the same persisted results with more control.

Build an RNA graph explicitly

The rebuilt store carries a completed docs_default pipeline run. This page starts from that run’s frozen cell and feature selections, then calls every graph stage explicitly. Identical calls reuse the run’s matching artifacts. Later sections create the branches they discuss.

DataStore has 3948 (5025) cells with 1 assays: RNA Cell metadata: 'I', 'ids', 'names', 'RNA_G2M_score', 'RNA_S_score', 'RNA_UMAP1', 'RNA_UMAP2', 'RNA_cell_cycle_phase', 'RNA_clusters', 'RNA_doublet_score', 'RNA_leiden_cluster', 'RNA_nCounts', 'RNA_nFeatures', 'RNA_paris_cluster', 'RNA_percentMito', 'RNA_percentRibo' RNA assay has 33538 features and following metadata: 'I', 'ids', 'names', 'dropOuts', 'feature_type', 'genome', 'nCells'

Started: 2026-10-11T18:54:50.575241Z | Wall time: 24.401 s

Retrieve the cell and gene selections used by the saved analysis:

{'cells': ArtifactRef(datastore, kind='cell_selection', artifact_id='2502cc7e1f32...'), 'genes': ArtifactRef(assay='RNA', kind='feature_selection', artifact_id='afcc1b8eca2b...')}

Started: 2026-10-11T18:55:14.982599Z | Wall time: 0.139 s

Each method returns a reference to its saved result. Pass it to the next method to keep the steps connected. We use 15 PCs to match the prepared example; reduction.pca defaults to 21.

ArtifactRef(assay='RNA', kind='reduction', artifact_id='4f781db22892...')

Started: 2026-10-11T18:55:15.124076Z | Wall time: 0.238 s

Build the neighbor index from PCA, then turn neighbor distances into connectivity:

ArtifactRef(assay='RNA', kind='connectivity_map', artifact_id='bee24e7f9e4e...')

Started: 2026-10-11T18:55:15.366194Z | Wall time: 31.137 s

The default neighbour count is 11. We will change only that value in the comparison below.

graph.load returns a sparse cell-by-cell connectivity matrix. Its default keeps the directed neighbour edges. For the diagnostics below, use symmetric=True to include a connection when either cell selects the other. This lets us count each cell’s neighbours in either direction. A cell can then have more than k neighbours because other cells may select it. In this symmetric matrix, nnz counts both directions of each connection.

((3948, 3948), 64990)

Started: 2026-10-11T18:55:46.507240Z | Wall time: 0.159 s

Histograms of symmetric graph node degree and edge weight with 11 neighbours per query. Degrees extend above 11 because incoming connections are included.

Started: 2026-10-11T18:55:46.669412Z | Wall time: 0.501 s

Check isolation and whether degree tracks QC metrics before treating the graph as ready for clustering:

active cells 3948.0 isolated cells 0.0 median degree 15.0 min degree 11.0 max degree 53.0 Name: graph coverage, dtype: float64

Started: 2026-10-11T18:55:47.174490Z | Wall time: 0.014 s

degreeRNA_nCountsRNA_nFeatures
degree1.0000000.0770020.015492
RNA_nCounts0.0770021.0000000.877195
RNA_nFeatures0.0154920.8771951.000000

Started: 2026-10-11T18:55:47.191820Z | Wall time: 0.070 s

The graph should contain a row for every selected cell, with finite positive weights on its stored connections. Zero entries mean there is no edge between those cells. The degree summary detects isolated cells, but a graph with no isolated cells can still contain separate components.

Separate components can represent distinct populations. Investigate unexpected fragmentation or a strong relationship between degree and QC metrics using markers, feature selection, PCA dimensions, and k. Increasing k just to join every component can connect unrelated cells.

Use the graph for a layout and clustering

embeddings.umap and embeddings.tsne require both the graph and its matching initialization. Leiden and Paris require the graph. Each returns an immutable artifact without adding cell-metadata columns. Use clusters.load_paris(ref) only when hierarchy diagnostics are needed. Resolution 0.5 matches the prepared PBMC analysis; Leiden’s default is 1.0.

PBMC UMAP coloured by ten Leiden clusters from the graph built with 11 neighbours. The layout shows several large groups and small isolated populations.

Started: 2026-10-11T18:55:47.265201Z | Wall time: 6.197 s

Branch by retaining both references

Suppose PCA and the approximate nearest-neighbor (ANN) index are expensive to recompute, but you want to compare two neighbor counts. Query the same index with a different k: only the neighbor and connectivity stages need new results. Keep both graph references so either analysis can continue independently.

True

Started: 2026-10-11T18:55:53.466002Z | Wall time: 0.496 s

Both branches remain complete, addressable artifacts. Downstream calls must receive one of them explicitly, so a parameter experiment cannot silently replace another branch.

Degree and edge weight both shift when every cell sees more neighbours:

k=11 nnz 64990 k=21 nnz 122020 Name: edges, dtype: int64

Started: 2026-10-11T18:55:53.965772Z | Wall time: 0.154 s

Histograms of symmetric graph node degree and edge weight after increasing the query to 21 neighbours. The degree distribution shifts toward more connections per cell.

Started: 2026-10-11T18:55:54.124102Z | Wall time: 0.469 s

To analyse the side branch, pass its exact graph reference. Retain both returned refs so neither branch replaces the other.

cluster 1 717 2 24 3 239 4 1312 5 353 6 319 7 564 8 149 9 256 10 15 Name: count, dtype: int64

Started: 2026-10-11T18:55:54.596356Z | Wall time: 0.255 s

Place both partitions on the shared k=11 UMAP so changes in group boundaries are visible, then compare their assignments with a crosstab:

Leiden assignments from graphs with 11 and 21 neighbours on the same UMAP. The larger-neighbour graph produces fewer clusters and changes boundaries within the large left-hand population.

Started: 2026-10-11T18:55:54.854561Z | Wall time: 1.150 s

k=2112345678910
k=11
1716000000000
2002390000000
30001237000000
400075353078000
5000003180000
6000001486000
7000000014900
8000000002560
912400000000
1000000000015

Started: 2026-10-11T18:55:56.008046Z | Wall time: 0.025 s

Cluster numbers can change even when the groups stay the same. Look for a row spread across several columns, or a column collecting several rows, to find splits or merges that depend on k. Review marker evidence before accepting those boundaries.

Recompute only what changed

Artifact identity includes the operation, scientific parameters, upstream inputs, and the operation’s revision. Calling an identical stage reuses its completed result. Changing k reuses normalization, PCA, and the ANN index but creates new neighbour and connectivity artifacts. Changing the cell or feature selection requires new downstream results. The saved results of the earlier analysis remain available.

Harmony changes the coordinates used to measure similarity. It reuses the existing PCA result, but its corrected coordinates need a new ANN index, neighbor search, and connectivity graph. Use it only when the study design supports technical batch correction:

# Correct PCA coordinates using a technical batch column.
corrected = ds.reduction.harmony(pca, ["technical_batch"])

# Build the neighbor index from corrected coordinates.
corrected_index = ds.graph.ann_index(corrected)

# Find neighbors in the corrected space.
corrected_neighbors = ds.graph.neighbors(corrected_index, k=21)

# Build connectivity from the corrected neighbors.
corrected_graph = ds.graph.connectivity(corrected_neighbors)

Use Why CyteArc records provenance to inspect complete lineage and Reuse results and compare analysis settings for reuse and invalidation patterns.