Skip to content

Note

Click here to download the full example code

Analyzing Neuron Morphology#

This tutorial will give you an overview of how to analyze neuron morphology.

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 TreeNeuron) but not for an image (i.e. a VoxelNeuron). 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, let's dive right in:

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 TreeNeuron)
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.TreeNeuron DA1_lPN_R 1734350788 4465 2705 599 618 266476.87500 4177.0 8 nanometer 2026-07-23 08:45:08.595936 /home/runner/work/navis/navis/navis/data/swc/1... 1734350788.swc
1 navis.TreeNeuron DA1_lPN_R 1734350908 4847 3042 735 761 304332.65625 6.0 8 nanometer 2026-07-23 08:45:08.601749 /home/runner/work/navis/navis/navis/data/swc/1... 1734350908.swc
2 navis.TreeNeuron DA1_lPN_R 722817260 4332 3136 633 656 274703.37500 NaN 8 nanometer 2026-07-23 08:45:08.607203 /home/runner/work/navis/navis/navis/data/swc/7... 722817260.swc
3 navis.TreeNeuron DA1_lPN_R 754534424 4696 3010 696 726 286522.46875 4.0 8 nanometer 2026-07-23 08:45:08.612689 /home/runner/work/navis/navis/navis/data/swc/7... 754534424.swc
4 navis.TreeNeuron DA1_lPN_R 754538881 4881 2943 626 642 291265.31250 701.0 8 nanometer 2026-07-23 08:45:08.618582 /home/runner/work/navis/navis/navis/data/swc/7... 754538881.swc

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

# Load a single MeshNeuron
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: 1291610825.1668386
Neuron volume: 661.3047424854216 micron ** 3

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

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

Out:

Mesh cable length: 1351124.75 nanometer

Note

The above .skeleton property is simply an automatically generated TreeNeuron 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 MeshNeurons as input, really use this skeleton representation under-the-hood.

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

m.skeleton.n_leafs

Out:

156
m.skeleton.n_branches

Out:

122

You may have already noticed here and in other examples the use of n_{property} (e.g. n.n_leafs). These are in fact generic: you can use any n_... and - assuming that property exists - NAVis will return a count:

m.n_vertices

Out:

6309
m.skeleton.n_nodes

Out:

1058
# 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 147.494362 1.297712 52615.137402 1 43.698252 3.699945 133.327778 3.365364e+06
1 555.074848 3.540705 53235.892128 1 42.986098 0.000000 176.226020 1.685668e+07
2 210.971713 1.063807 52176.483659 1 72.242903 3.699802 223.577816 1.420405e+07
3 120.534637 1.310145 51521.007279 1 56.487603 7.399605 154.409208 1.211933e+06
4 58.374999 1.025037 54367.973139 1 66.342890 7.399890 127.752521 1.478380e+06
# 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#

For an example of a Sholl analyses, check out the MICrONS tutorial.

Branch, Path & Soma-Exit Angles#

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

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

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
<class 'navis.core.neuronlist.NeuronList'> containing 3 neurons (875.6KiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units created_at origin file
0 navis.TreeNeuron 601506507_transformed ace-die-lax/601506507_transformed 3680 None 30 32 6012.066895 1 1 dimensionless 2026-07-23 08:45:12.315610 https://download.brainimagelibrary.org/03/3f/0... 601506507_transformed.swc
1 navis.TreeNeuron 601790961_transformed ace-die-lay/601790961_transformed 12333 None 163 169 19280.503906 1 1 dimensionless 2026-07-23 08:45:12.485274 https://download.brainimagelibrary.org/03/3f/0... 601790961_transformed.swc
2 navis.TreeNeuron 601803754_transformed add-wag/601803754_transformed 21340 None 373 381 35083.648438 1 1 dimensionless 2026-07-23 08:45:12.601031 https://download.brainimagelibrary.org/03/3f/0... 601803754_transformed.swc

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()
node_id branch_angle compartment
0 23 72.793845 dendrite
1 30 61.749779 dendrite
2 75 146.342239 dendrite
3 868 101.609305 dendrite
4 877 52.838753 dendrite

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)")

tutorial morpho 01 analyze

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()

Out:

neuron
ace-die-lax/601506507_transformed    137.764003
ace-die-lay/601790961_transformed    100.122529
add-wag/601803754_transformed         90.594849
Name: soma_exit_angle, dtype: float64

Geodesic Distances#

Working with Euclidean distances is straight forward and we won't cover this extensively but here is an example where we are measuring the average distances between a node and its parent (= 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})"
)

Out:

Mean distance between nodes: 477.56 nanometer (+/- 361.10 nanometer)

What if you wanted to know the distance between the soma and all terminal nodes? In that case Euclidean distance would be insufficient as the neuron is not a straight line. Instead, you need the geodesic, the "along-the-arbor" distance.

NAVis comes with a couple functions that help you get geodesic distances. For single node-to-node queries, navis.dist_between should be 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}")

Out:

Euclidean distance between soma and terminal node 465: 444096.16 nanometer

Let's visualize this:

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",
)

tutorial morpho 01 analyze

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()
465 548 618 683 745 789 832 872 911 949 987 1024 1060 1095 1124 1153 1181 1209 1237 1264 1290 1316 1341 1366 1390 1413 1436 1458 1480 1502 1523 1544 1565 1586 1606 1626 1646 1666 1686 1706 ... 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465
465 0.000000 54395.707031 53556.406250 54489.726562 53685.769531 52679.984375 53139.882812 20944.501953 53065.273438 53873.632812 53739.347656 6767.158691 52967.968750 54020.863281 21164.632812 52185.433594 53729.804688 53460.152344 53023.214844 54051.707031 52663.621094 52178.281250 54002.679688 53184.105469 52079.449219 6315.998535 7808.719727 5656.241211 52686.707031 52757.835938 53003.398438 52983.808594 52802.109375 51182.585938 53735.332031 52967.058594 53235.902344 52307.960938 52413.675781 52355.273438 ... 52492.808594 52492.578125 52813.566406 52441.593750 52303.636719 52297.687500 52312.007812 52208.507812 52498.488281 51904.691406 51867.289062 51785.121094 52274.488281 51760.929688 51623.722656 51596.882812 52182.867188 51514.605469 51560.789062 51640.222656 51751.464844 51603.109375 51434.824219 51565.632812 51212.375000 51195.523438 50818.179688 50537.144531 50555.058594 50795.398438 50402.960938 50363.976562 49936.089844 49895.070312 55185.277344 54742.765625 55158.503906 55950.523438 56111.789062 55266.164062
548 54395.707031 0.000000 8980.969727 10787.026367 6228.693848 6866.540039 8564.446289 37619.898438 8489.836914 6416.556641 10036.644531 53636.648438 5732.234863 10318.163086 40115.660156 5461.810547 6272.729492 6003.076172 8447.777344 10349.005859 8960.916992 7602.847168 6545.604492 3736.930420 8376.746094 51252.972656 49521.371094 52525.730469 8111.272461 9055.134766 8427.961914 8408.371094 8226.673828 6607.148926 2236.750977 5509.983887 5778.826172 5072.226562 8710.974609 7779.837402 ... 7917.371582 7917.141602 9110.863281 7866.156738 7728.200195 4840.612305 4854.934570 4751.431641 8795.787109 8201.990234 2811.261230 4549.386719 8571.788086 7185.492676 4387.987793 7021.448730 8480.166016 3128.714844 4103.715332 7064.787109 7176.028320 7900.406250 6859.390137 7862.931641 7509.673828 7492.823242 5004.737793 4116.092285 4309.074707 7092.696289 4589.518555 5788.540527 5360.653809 5319.634766 12127.772461 11685.260742 12101.001953 12893.017578 13054.287109 12208.662109
618 53556.406250 8980.969727 0.000000 9947.724609 8271.031250 7265.245605 4698.465332 36780.597656 3951.850586 8458.894531 9197.343750 52797.347656 7553.233398 9478.862305 39276.359375 6770.696777 8315.067383 8045.413574 3299.611328 9509.705078 8121.616211 3736.865967 8587.942383 7769.369141 7537.445312 50413.671875 48682.070312 51686.429688 3573.286621 8215.833984 4561.980957 4542.390137 3688.687988 4879.403809 8320.593750 7552.321289 7821.163574 6893.225098 7871.673828 3913.856201 ... 4051.390625 4051.160889 8271.562500 2717.990967 3862.219238 6882.949707 6897.271973 6793.769043 7956.486328 7362.689453 6452.551270 6370.385254 7732.487305 3319.511719 6208.986328 3623.443115 7640.864746 6099.867676 6146.052734 3198.806152 3310.047363 7061.105469 2686.695557 7023.630859 6670.372559 6653.522461 5403.443359 5122.408691 5140.319824 6253.395020 4988.224121 4060.795654 3828.188232 3866.356201 11288.470703 10845.959961 11261.701172 12053.716797 12214.986328 11369.361328
683 54489.726562 10787.026367 9947.724609 0.000000 10077.087891 9071.301758 9531.201172 37713.921875 9456.591797 10264.951172 5057.882812 53730.667969 9359.290039 3253.420654 40209.679688 8576.752930 10121.123047 9851.469727 9414.532227 3284.263428 4781.085938 8569.602539 10393.998047 9575.425781 4946.421387 51346.996094 49615.394531 52619.750000 9078.028320 3605.263184 9394.716797 9375.126953 9193.428711 7573.904297 10126.650391 9358.377930 9627.219727 8699.281250 3732.213623 8746.592773 ... 8884.126953 8883.897461 4132.102051 8832.912109 8694.956055 8689.005859 8703.328125 8599.825195 2517.971680 4022.159180 8258.607422 8176.441406 3593.026855 8152.248047 8015.042480 7988.204102 3501.404297 7905.924316 7952.109375 8031.542480 8142.783691 4218.177246 7826.145508 3149.859375 3827.444580 3810.594238 7209.499512 6928.465332 6946.376465 3987.779053 6794.280273 6755.295898 6327.409668 6286.390137 12221.793945 11779.282227 12195.023438 12987.039062 13148.308594 12302.683594
745 53685.769531 6228.693848 8271.031250 10077.087891 0.000000 6156.602051 7854.507812 36909.960938 7779.898438 4485.482910 9326.706055 52926.710938 5022.296875 9608.224609 39405.722656 4751.872070 2609.798096 4072.002197 7737.838867 9639.067383 8250.978516 6892.908691 4614.530762 5017.093262 7666.808105 50543.035156 48811.433594 51815.792969 7401.334473 8345.196289 7718.023438 7698.432617 7516.735352 5897.210449 5568.317871 3578.909668 3847.752197 4362.288574 8001.036621 7069.898926 ... 7207.433105 7207.203613 8400.924805 7156.218262 7018.261719 2909.538086 2625.473145 2820.357666 8085.849121 7492.051758 3700.275391 3839.448486 7861.850098 6475.554199 3678.049561 6311.510254 7770.227539 3347.592041 2327.137207 6354.848633 6466.089844 7190.468262 6149.451660 7152.993652 6799.735352 6782.884766 4294.799805 3406.154053 3599.136719 6382.757812 3879.580078 5078.602051 4650.715820 4609.696777 11417.833984 10975.322266 11391.063477 12183.080078 12344.348633 11498.723633

5 rows × 618 columns

Let's see if we can visualize any clusters using 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([])

tutorial morpho 01 analyze

Out:

[]

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: ( 0 minutes 5.871 seconds)

Download Python source code: tutorial_morpho_01_analyze.py

Download Jupyter notebook: tutorial_morpho_01_analyze.ipynb

Gallery generated by mkdocs-gallery