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.815 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 2024-09-18 12:16:58.822426 /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 2024-09-18 12:16:58.829825 /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 2024-09-18 12:16:58.836650 /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 2024-09-18 12:16:58.843681 /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 2024-09-18 12:16:58.853488 /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.1668377
Neuron volume: 661.3047424854211 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:

/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:198: DeprecationWarning:

Graph.clusters() is deprecated; use Graph.connected_components() instead

/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:228: DeprecationWarning:

Graph.shortest_paths() is deprecated; use Graph.distances() instead

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:

155
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 84.610286 1.320717 7083.656132 1 61.481414 11.867318 148.842288 1.420165e+06
1 276.524917 2.589908 41381.328436 1 58.348518 3.699802 165.413996 6.111648e+06
2 141.448954 1.079153 12560.860744 1 86.103133 24.133240 156.117439 4.873987e+06
3 59.935134 1.122367 11144.594281 1 32.241229 3.699802 85.624280 4.333826e+05
4 141.513383 1.362527 38527.170130 1 44.703457 9.421692 139.731548 1.472218e+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")

plot 01 morpho analyze

Sholl Analysis#

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

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

plot 01 morpho 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.710938 53556.410156 54489.730469 53685.773438 52679.988281 53139.886719 20944.503906 53065.277344 53873.636719 53739.351562 6767.158691 52967.976562 54020.867188 21164.632812 52185.437500 53729.808594 53460.156250 53023.218750 54051.710938 52663.625000 52178.289062 54002.683594 53184.109375 52079.453125 6315.998535 7808.719727 5656.241211 52686.710938 52757.839844 53003.402344 52983.812500 52802.113281 51182.589844 53735.335938 52967.062500 53235.906250 52307.964844 52413.679688 52355.277344 ... 52492.812500 52492.582031 52813.570312 52441.597656 52303.640625 52297.691406 52312.011719 52208.511719 52498.492188 51904.695312 51867.292969 51785.125000 52274.492188 51760.933594 51623.726562 51596.890625 52182.871094 51514.609375 51560.792969 51640.226562 51751.468750 51603.113281 51434.828125 51565.636719 51212.378906 51195.531250 50818.183594 50537.148438 50555.062500 50795.402344 50402.964844 50363.980469 49936.093750 49895.074219 55185.277344 54742.765625 55158.507812 55950.523438 56111.792969 55266.167969
548 54395.710938 0.000000 8980.969727 10787.025391 6228.693848 6866.540039 8564.446289 37619.902344 8489.836914 6416.556641 10036.644531 53636.648438 5732.234375 10318.162109 40115.664062 5461.810547 6272.729492 6003.076172 8447.777344 10349.005859 8960.916992 7602.846680 6545.604492 3736.930176 8376.746094 51252.976562 49521.375000 52525.734375 8111.272461 9055.134766 8427.961914 8408.371094 8226.673828 6607.148438 2236.750977 5509.983398 5778.826172 5072.226562 8710.974609 7779.836914 ... 7917.371094 7917.141602 9110.863281 7866.156250 7728.200195 4840.611816 4854.934570 4751.431641 8795.787109 8201.990234 2811.261230 4549.386719 8571.788086 7185.492188 4387.987305 7021.448242 8480.166016 3128.714600 4103.715332 7064.787109 7176.027832 7900.406250 6859.389648 7862.931641 7509.673828 7492.823242 5004.737305 4116.092285 4309.074707 7092.696289 4589.518066 5788.540527 5360.653809 5319.634766 12127.771484 11685.260742 12101.000977 12893.017578 13054.287109 12208.661133
618 53556.410156 8980.969727 0.000000 9947.724609 8271.031250 7265.245117 4698.465332 36780.601562 3951.851074 8458.894531 9197.342773 52797.347656 7553.233398 9478.861328 39276.363281 6770.696777 8315.067383 8045.413086 3299.611572 9509.704102 8121.616211 3736.865967 8587.941406 7769.369141 7537.445312 50413.675781 48682.074219 51686.433594 3573.287109 8215.833008 4561.980957 4542.390625 3688.688477 4879.403809 8320.592773 7552.320801 7821.163086 6893.224609 7871.673828 3913.856445 ... 4051.390625 4051.160645 8271.562500 2717.991211 3862.219238 6882.949219 6897.271973 6793.769531 7956.486328 7362.688965 6452.550781 6370.384766 7732.486816 3319.511719 6208.986328 3623.442871 7640.864258 6099.867676 6146.052734 3198.806152 3310.047363 7061.105469 2686.695557 7023.630371 6670.372559 6653.521973 5403.443359 5122.408691 5140.319824 6253.395020 4988.223633 4060.795410 3828.188232 3866.356201 11288.469727 10845.958984 11261.699219 12053.715820 12214.984375 11369.359375
683 54489.730469 10787.025391 9947.724609 0.000000 10077.087891 9071.301758 9531.201172 37713.921875 9456.591797 10264.951172 5057.882812 53730.667969 9359.289062 3253.420898 40209.683594 8576.752930 10121.123047 9851.469727 9414.532227 3284.263672 4781.085938 8569.601562 10393.998047 9575.425781 4946.421387 51346.996094 49615.394531 52619.753906 9078.027344 3605.263184 9394.716797 9375.126953 9193.429688 7573.903809 10126.649414 9358.376953 9627.219727 8699.281250 3732.213379 8746.591797 ... 8884.126953 8883.896484 4132.102051 8832.912109 8694.955078 8689.005859 8703.328125 8599.826172 2517.971680 4022.159180 8258.607422 8176.441406 3593.026855 8152.247559 8015.042480 7988.203613 3501.404297 7905.923828 7952.109375 8031.541992 8142.783203 4218.177246 7826.145020 3149.859375 3827.444580 3810.593994 7209.499512 6928.464844 6946.375977 3987.779053 6794.280273 6755.295898 6327.409180 6286.390137 12221.791992 11779.281250 12195.021484 12987.038086 13148.306641 12302.681641
745 53685.773438 6228.693848 8271.031250 10077.087891 0.000000 6156.601562 7854.507812 36909.964844 7779.898438 4485.483398 9326.706055 52926.710938 5022.296875 9608.224609 39405.726562 4751.872070 2609.798096 4072.002441 7737.838867 9639.067383 8250.979492 6892.909180 4614.530762 5017.093750 7666.808594 50543.039062 48811.437500 51815.796875 7401.334473 8345.196289 7718.023926 7698.433105 7516.735840 5897.210449 5568.317871 3578.909668 3847.752197 4362.288086 8001.037109 7069.899414 ... 7207.433594 7207.203125 8400.925781 7156.218750 7018.261719 2909.538086 2625.473145 2820.357910 8085.849609 7492.052246 3700.275391 3839.448486 7861.850098 6475.554199 3678.049561 6311.510254 7770.227539 3347.592285 2327.137207 6354.848633 6466.089844 7190.468750 6149.451660 7152.993652 6799.735840 6782.885254 4294.799805 3406.154053 3599.136719 6382.758301 3879.580078 5078.602539 4650.715820 4609.696777 11417.833008 10975.322266 11391.062500 12183.079102 12344.347656 11498.722656

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

plot 01 morpho 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 2.554 seconds)

Download Python source code: plot_01_morpho_analyze.py

Download Jupyter notebook: plot_01_morpho_analyze.ipynb

Gallery generated by mkdocs-gallery