Skip to content

Note

Click here to download the full example code

Analyzing Neuron Morphology#

Measure morphometrics — segment lengths, geodesic distances and branch angles.

Disclaimer: As you might imagine some properties can be gathered for all/most neuron types while others will only work on specific types. For example, topological properties such as cable length, branch points, etc. are easy to get for a skeleton (i.e. a Skeleton) but not for an image (i.e. a Voxels). Make sure to check the respective function's docstring for details!

Basic Properties#

NAVis provides some basic morphometrics as neuron properties. Others need to be computed using a specific function (e.g. navis.tortuosity) - mostly because they require/allow some parameters to be set.

With that in mind:

import navis
import matplotlib.pyplot as plt

import seaborn as sns

sns.set_color_codes("muted")

Accessing attributes for a single neuron:

# Load a single skeleton (i.e. a Skeleton)
n = navis.example_neurons(n=1, kind="skeleton")
print(f"This neuron has {n.cable_length} of cable")

Out:

This neuron has 266476.875 of cable

If your neuron has its units properties set, you can also combine those:

print(f"This neuron has {(n.cable_length * n.units).to('microns')} of cable")

Out:

This neuron has 2131.815185546875 micron of cable

Accessing the same properties via a navis.NeuronList will return an array:

nl = navis.example_neurons(kind="skeleton")
print(f"Cable lengths for neurons in this list:\n{nl.cable_length}")

Out:

Cable lengths for neurons in this list:
[266476.88 304332.66 274703.38 286522.47 291265.3 ]

navis.NeuronList.summary is a useful way to collect some of the basic parameters:

df = nl.summary()
df.head()
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units created_at origin file
0 navis.Skeleton DA1_lPN_R 1734350788 4465 2705 599 618 266476.87500 4177.0 8 nanometer 2026-11-03 14:35:30.710412 /home/runner/work/navis/navis/navis/data/swc/1... 1734350788.swc
1 navis.Skeleton DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 6.0 8 nanometer 2026-11-03 14:35:30.716102 /home/runner/work/navis/navis/navis/data/swc/1... 1734350908.swc
2 navis.Skeleton DA1_lPN_R 722817260 4332 3136 633 656 274703.37500 NaN 8 nanometer 2026-11-03 14:35:30.721284 /home/runner/work/navis/navis/navis/data/swc/7... 722817260.swc
3 navis.Skeleton DA1_lPN_R 754534424 4696 3010 696 726 286522.46875 4.0 8 nanometer 2026-11-03 14:35:30.727390 /home/runner/work/navis/navis/navis/data/swc/7... 754534424.swc
4 navis.Skeleton DA1_lPN_R 754538881 4881 2943 626 642 291265.31250 701.0 8 nanometer 2026-11-03 14:35:30.732945 /home/runner/work/navis/navis/navis/data/swc/7... 754538881.swc

For Meshes the available properties look different. For example, you can get its volume:

# Load a single Mesh
m = navis.example_neurons(n=1, kind="mesh")
print(f"Neuron volume: {m.volume}")

# Again, we can use the `units` property to convert:
print(f"Neuron volume: {(m.volume * n.units **3).to('microns ** 3')}")

Out:

Neuron volume: 1362810177.9803793
Neuron volume: 697.7588111259544 micron ** 3

For topological properties, we need to convert to skeleton. The fastest way is to simply access the Mesh's .skeleton property:

print(f"Mesh cable length: {m.skeleton.cable_length * m.units}")

Out:

Mesh cable length: 1562069.75 nanometer

Note

The above .skeleton property is simply an automatically generated Skeleton representation of the mesh. It uses sensible defaults but as said initially: it's good practice to create and check the skeleton yourself via navis.skeletonize.

Importantly, some NAVis functions (e.g. navis.segment_analysis, see below) that accept Meshes as input, really use this skeleton representation under-the-hood.

The skeleton representation of the mesh lets us access many topological properties:

m.skeleton.n_leafs

Out:

198
m.skeleton.n_branches

Out:

171

The n_ prefix

You may have noticed the n_{property} pattern (e.g. n.n_leafs). It's generic: prefix any existing attribute with n_ and - assuming that property exists - NAVis returns its count.

m.n_vertices

Out:

17369
m.skeleton.n_nodes

Out:

1931
# Illustrate with a random property
m.my_counts = [1, 2, 3, 4, 5]
m.n_my_counts

Out:

5

Segment Analysis#

navis.segment_analysis is a great entry point for collecting a bunch of morphometrics for your neuron(s) of interest. It returns Strahler index, cable length, distance to root, radius and tortuosity for each linear segment:

sa = navis.segment_analysis(m)
sa.head()
length tortuosity root_dist strahler_index radius_mean radius_min radius_max volume
0 77.984956 1.537652 55237.847656 1 39.059020 12.668204 79.172447 2.278803e+05
1 2067.024812 1.217699 54509.492188 1 61.195691 27.052340 137.975689 2.749390e+07
2 262.951887 1.410754 52362.476562 1 71.441763 8.103156 174.305057 9.592912e+06
3 742.814301 1.064692 10287.462891 1 71.140240 42.520486 125.929214 1.256522e+07
4 648.992907 1.574694 527.833679 1 68.775577 0.000000 422.868212 1.777039e+07
# See if segment length correlates with radius
ax = sns.scatterplot(
    data=sa, x="length", y="radius_mean", size="strahler_index", alpha=0.7
)
ax.set_xscale("log")
ax.set_yscale("log")

tutorial morpho 01 analyze

Sholl Analysis#

  • Sholl analysis


    A worked Sholl analysis lives in the MICrONS tutorial.

    MICrONS tutorial

Branch, Path & Soma-Exit Angles#

NAVis can also measure the angles in a neuron's geometry:

Function Measures
navis.branch_angles angle between child branches at each branch point
navis.path_angles bend at each continuation (slab) node
navis.root_angles each segment's angle relative to the root direction
navis.soma_exit_angles angles between the neurites leaving the soma

These are most informative for neurons with a clear dendritic/axonal layout such as vertebrate cortical cells. The example neurons we have used so far are insect neurons, so we instead pull a few mouse cortical reconstructions from the Brain Image Library (BIL).

Fetches remote data

The branch-angle examples below download a handful of reconstructions from the Brain Image Library, so this section needs an internet connection.

import pandas as pd

import navis.interfaces.brain_image_library as bil

# A handful of patch-seq mouse cortical cells (the same dataset as the "Cortical Neurons"
# plotting tutorial). These SWCs carry a `label` column: 1 = soma, 2 = axon, 3 = dendrite.
ids = [601506507, 601790961, 601803754]
datasets = [bil.query("specimen", "localid", str(i))[0] for i in ids]
cortical = bil.get_neurons(datasets, pattern="*_transformed.swc")

# The angle metrics are measured relative to the root, so root each neuron at its soma:
for cn in cortical:
    cn.reroot(cn.soma, inplace=True)

cortical
Traceback (most recent call last):
  File "/home/runner/work/navis/navis/docs/examples/2_morpho/tutorial_morpho_01_analyze.py", line 166, in <module>
    cortical = bil.get_neurons(datasets, pattern="*_transformed.swc")
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/navis/navis/navis/interfaces/brain_image_library.py", line 1206, in get_neurons
    available = x if _is_file_table(x) else list_files(x, **list_kwargs)
                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/navis/navis/navis/interfaces/brain_image_library.py", line 842, in list_files
    _list_files_single(
  File "/home/runner/work/navis/navis/navis/interfaces/brain_image_library.py", line 934, in _list_files_single
    raise BILError(
navis.interfaces.brain_image_library.BILError: Failed to list dataset 'ace-die-lax' at https://download.brainimagelibrary.org/03/3f/033fee1bdd563364/601506507/: HTTPSConnectionPool(host='download.brainimagelibrary.org', port=443): Max retries exceeded with url: /03/3f/033fee1bdd563364/601506507/ (Caused by ConnectTimeoutError(<HTTPSConnection(host='download.brainimagelibrary.org', port=443) at 0x7f2b6c500fd0>, 'Connection to download.brainimagelibrary.org timed out. (connect timeout=60)'))
The BIL download server may be down or unreachable from this machine - check https://download.brainimagelibrary.org and try again.

navis.branch_angles returns one row per branch point, so the result is effectively a distribution we can summarize or plot. Let's compute it per neuron and use the label column to split the branch angles by compartment (axon vs dendrite):

frames = []
for cn in cortical:
    b = navis.branch_angles(cn)
    # Map each branch point onto its compartment label (per-neuron, since node IDs
    # are only unique within a neuron)
    b["compartment"] = b.node_id.map(cn.nodes.set_index("node_id").label)
    frames.append(b)

ba = pd.concat(frames, ignore_index=True)
ba["compartment"] = ba.compartment.map({2: "axon", 3: "dendrite"})
ba.head()

Now we can compare the branch-angle distributions between axon and dendrite:

ax = sns.violinplot(
    data=ba.dropna(subset=["compartment"]),
    x="compartment",
    y="branch_angle",
    cut=0,
)
_ = ax.set_ylabel("branch angle (degrees)")

The other angle functions behave the same way. For instance, navis.soma_exit_angles gives the angles between the neurites leaving the soma; MorphoPy's mean_soma_exit_angle statistic is simply the mean of these per neuron:

sea = navis.soma_exit_angles(cortical)
sea.groupby("neuron").soma_exit_angle.mean()

Geodesic Distances#

Euclidean vs geodesic distance

Euclidean distance is the straight-line distance between two points. Geodesic distance is measured along the arbor - following the neuron's cable. Because a neuron is rarely a straight line, the two differ, and questions like "how far is this terminal from the soma along the neuron?" call for the geodesic distance.

Euclidean distances are straightforward. Here we measure the average distance between a node and its parent (i.e. the sampling rate):

import numpy as np

# Get nodes but remove the root (has no parent)
nodes = nl[0].nodes[nl[0].nodes.parent_id > 0]

# Get the x/y/z coordinates of all nodes (except root)
node_locs = nodes[["x", "y", "z"]].values

# For each node, get its parent's location
parent_locs = (
    nl[0].nodes.set_index("node_id").loc[nodes.parent_id.values, ["x", "y", "z"]].values
)

# Calculate Euclidean distances
distances = np.sqrt(np.sum((node_locs - parent_locs) ** 2, axis=1))

# Use the neuron's units to convert into nm
distances = distances * nl[0].units

print(
    f"Mean distance between nodes: {np.mean(distances):.2f} (+/- {np.std(distances):.2f})"
)

For the distance between the soma and all terminal nodes we need the geodesic distance (see the box above) - the neuron is not a straight line. For single node-to-node queries, navis.dist_between is sufficient:

n = nl[0]

end = n.nodes[n.nodes.type == "end"].node_id.values[0]

d_geo = navis.dist_between(n, n.soma, end) * n.units

print(f"Euclidean distance between soma and terminal node {end}: {d_geo:.2f}")

Visualizing the two distances:

import networkx as nx

# First we need to find the path between the soma and the terminal node
path = nx.shortest_path(n.graph.to_undirected(), n.soma, end)

# Get coordinates for the path
path_co = n.nodes.set_index("node_id").loc[path, ["x", "y", "z"]].copy()

# Add a small offset
path_co.x += 500
path_co.y -= 500

# Plot neuron
fig, ax = navis.plot2d(n, c="blue", method="2d", view=("x", "-z"))

# Add geodesic path
ax.plot(path_co.x, path_co.z, c="r", ls="--")

# Add Euclidean path
end_loc = n.nodes.set_index("node_id").loc[end, ["x", "y", "z"]]
soma_loc = n.nodes.set_index("node_id").loc[n.soma, ["x", "y", "z"]]
ax.plot([soma_loc.x, end_loc.x], [soma_loc.z, end_loc.z], c="g", ls="--")

d_eucl = np.sqrt(np.sum((end_loc - soma_loc) ** 2)) * n.units

# Annotate distances
_ = ax.text(
    x=0.1,
    y=0.3,
    s=f"Euclidean distance:\n{d_eucl.to_compact():.0f}",
    transform=ax.transAxes,
    c="g",
)
_ = ax.text(
    x=0.85,
    y=0.5,
    s=f"Geodesic distance:\n{d_geo.to_compact():.0f}",
    transform=ax.transAxes,
    c="r",
    ha="right",
)

If you want to look at geodesic distances across many nodes, you should consider using navis.geodesic_matrix. To demonstrate, let's generate a geodesic distance matrix between all terminal nodes:

# Calculate distances from all end nodes to all other nodes
ends = n.nodes[n.nodes.type == "end"].node_id.values
m = navis.geodesic_matrix(n, from_=ends)

# Subset to only end-nodes-to-end_nodes
m = m.loc[ends, ends]

m.head()

Visualizing any clusters with a seaborn clustermap:

import seaborn as sns

from scipy.cluster.hierarchy import linkage
from scipy.spatial.distance import squareform

# Generate a linkage from the distances
Z = linkage(squareform(m, checks=False), method="ward")

# Plot
cm = sns.clustermap(m, cmap="Greys", col_linkage=Z, row_linkage=Z)

cm.ax_heatmap.set_xticks([])
cm.ax_heatmap.set_yticks([])

As you can see in the heatmap, the dendrites and the axon nicely separate.

That's it for now! Please see the NBLAST tutorial for morphological comparisons using NBLAST and the API reference for a full list of morphology-related functions.

Total running time of the script: ( 4 minutes 4.425 seconds)

Download Python source code: tutorial_morpho_01_analyze.py

Download Jupyter notebook: tutorial_morpho_01_analyze.ipynb

Gallery generated by mkdocs-gallery