Skip to content

Note

Click here to download the full example code

Peyrache et al (2015) Tutorial

This tutorial demonstrates how we use Pynapple to generate Figure 4a in the publication. The NWB file for the example is hosted on OSF. We show below how to stream it. The entire dataset can be downloaded here.

See the documentation of Pynapple for instructions on installing the package.

This tutorial was made by Dhruv Mehrotra and Guillaume Viejo.

Warning

This tutorial uses seaborn and matplotlib for displaying the figure

You can install all with pip install matplotlib seaborn tqdm

mkdocs_gallery_thumbnail_number = 2

Now, import the necessary libraries:

import numpy as np
import pandas as pd
import pynapple as nap
import scipy.ndimage
import matplotlib.pyplot as plt
import seaborn as sns
import requests, math, os
import tqdm

custom_params = {"axes.spines.right": False, "axes.spines.top": False}
sns.set_theme(style="ticks", palette="colorblind", font_scale=1.5, rc=custom_params)

Downloading the data

It's a small NWB file.

path = "Mouse32-140822.nwb"
if path not in os.listdir("."):
    r = requests.get(f"https://osf.io/jb2gd/download", stream=True)
    block_size = 1024*1024
    with open(path, 'wb') as f:
        for data in tqdm.tqdm(r.iter_content(block_size), unit='MB', unit_scale=True,
            total=math.ceil(int(r.headers.get('content-length', 0))//block_size)):
            f.write(data)

Parsing the data

The first step is to load the data and other relevant variables of interest

data = nap.load_file(path)  # Load the NWB file for this dataset

What does this look like ?

print(data)

Out:

Mouse32-140822
┍━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━┑
 Keys                   Type        ┝━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┥
 units                  TsGroup      sws                    IntervalSet  rem                    IntervalSet  position_time_support  IntervalSet  epochs                 IntervalSet  ry                     Tsd         ┕━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━┙

Head-Direction Tuning Curves

To plot Head-Direction Tuning curves, we need the spike timings and the orientation of the animal. These quantities are stored in the variables 'units' and 'ry'.

spikes = data["units"]  # Get spike timings
epochs = data["epochs"]  # Get the behavioural epochs (in this case, sleep and wakefulness)
angle = data["ry"]  # Get the tracked orientation of the animal

What does this look like ?

print(spikes)

Out:

  Index      rate  location      group
-------  --------  ----------  -------
      0   2.96981  thalamus          1
      1   2.42638  thalamus          1
      2   5.93417  thalamus          1
      3   5.04432  thalamus          1
      4   0.30207  adn               2
      5   0.87042  adn               2
      6   0.36154  adn               2
      7  10.5174   adn               2
      8   2.62475  adn               2
      9   2.55818  adn               2
     10   7.06715  adn               2
     11   0.37911  adn               2
     12   1.58248  adn               2
     13   4.87837  adn               2
     14   8.47337  adn               2
     15   0.23723  adn               3
     16   0.26593  adn               3
     17   6.1304   adn               3
     18  11.0137   adn               3
     19   5.23346  adn               3
     20   6.19514  adn               3
     21   2.85458  adn               3
     22   9.71401  adn               3
     23   1.70552  adn               3
     24  19.6539   adn               3
     25   3.87855  adn               3
     26   4.0242   adn               3
     27   0.68935  adn               3
     28   1.78011  adn               4
     29   4.23006  adn               4
     30   2.15215  adn               4
     31   0.58829  adn               4
     32   1.12899  adn               4
     33   5.26316  adn               4
     34   1.57122  adn               4
     35   4.74811  thalamus          5
     36   1.3077   thalamus          5
     37   0.76736  thalamus          5
     38   2.02066  thalamus          5
     39  27.2073   thalamus          5
     40   7.28227  thalamus          5
     41   0.87805  thalamus          5
     42   1.02061  thalamus          5
     43   6.84913  thalamus          6
     44   0.94002  thalamus          6
     45   0.55768  thalamus          6
     46   1.15056  thalamus          6
     47   0.46084  thalamus          6
     48   0.19287  thalamus          7

Here, rate is the mean firing rate of the unit. Location indicates the brain region the unit was recorded from, and group refers to the shank number on which the cell was located.

This dataset contains units recorded from the anterior thalamus. Head-direction (HD) cells are found in the anterodorsal nucleus of the thalamus (henceforth referred to as ADn). Units were also recorded from nearby thalamic nuclei in this animal. For the purposes of our tutorial, we are interested in the units recorded in ADn. We can restrict ourselves to analysis of these units rather easily, using Pynapple.

spikes_adn = spikes.getby_category("location")["adn"]  # Select only those units that are in ADn

What does this look like ?

print(spikes_adn)

Out:

  Index      rate  location      group
-------  --------  ----------  -------
      4   0.30207  adn               2
      5   0.87042  adn               2
      6   0.36154  adn               2
      7  10.5174   adn               2
      8   2.62475  adn               2
      9   2.55818  adn               2
     10   7.06715  adn               2
     11   0.37911  adn               2
     12   1.58248  adn               2
     13   4.87837  adn               2
     14   8.47337  adn               2
     15   0.23723  adn               3
     16   0.26593  adn               3
     17   6.1304   adn               3
     18  11.0137   adn               3
     19   5.23346  adn               3
     20   6.19514  adn               3
     21   2.85458  adn               3
     22   9.71401  adn               3
     23   1.70552  adn               3
     24  19.6539   adn               3
     25   3.87855  adn               3
     26   4.0242   adn               3
     27   0.68935  adn               3
     28   1.78011  adn               4
     29   4.23006  adn               4
     30   2.15215  adn               4
     31   0.58829  adn               4
     32   1.12899  adn               4
     33   5.26316  adn               4
     34   1.57122  adn               4

Let's compute some head-direction tuning curves. To do this in Pynapple, all you need is a single line of code!

Plot firing rate of ADn units as a function of heading direction, i.e. a head-direction tuning curve

tuning_curves = nap.compute_1d_tuning_curves(
    group=spikes_adn, 
    feature=angle, 
    nb_bins=61, 
    ep = epochs['wake'],
    minmax=(0, 2 * np.pi)
    )

What does this look like ?

print(tuning_curves)

Out:

                4         5   ...         33        34
0.051502  0.255172  0.127586  ...   5.273551  1.786203
0.154505  0.300635  0.000000  ...   8.380200  1.954127
0.257508  0.189885  0.094943  ...  10.506971  3.892643
0.360511  0.498062  0.052428  ...  12.556397  5.321396
0.463514  0.362941  0.111674  ...  15.271458  8.431408
...            ...       ...  ...        ...       ...
5.819672  0.063460  0.158650  ...   0.285571  0.507681
5.922675  0.024772  0.123861  ...   0.767941  0.272495
6.025678  0.000000  0.112276  ...   0.954343  0.196482
6.128681  0.000000  0.138009  ...   1.021266  0.441629
6.231684  0.067699  0.101548  ...   2.234055  0.609288

[61 rows x 31 columns]

Each row indicates an angular bin (in radians), and each column corresponds to a single unit. Let's compute the preferred angle quickly as follows:

pref_ang = tuning_curves.idxmax()

For easier visualization, we will colour our plots according to the preferred angle of the cell. To do so, we will normalize the range of angles we have, over a colourmap.

norm = plt.Normalize()  # Normalizes data into the range [0,1]
color = plt.cm.hsv(norm([i / (2 * np.pi) for i in pref_ang.values]))  # Assigns a colour in the HSV colourmap for each value of preferred angle
color = pd.DataFrame(index=pref_ang.index, data = color, columns = ['r', 'g', 'b', 'a'])

To make the tuning curves look nice, we will smooth them before plotting, using this custom function:

from scipy.ndimage import gaussian_filter1d
def smoothAngularTuningCurves(tuning_curves, sigma=2):

    tmp = np.concatenate((tuning_curves.values, tuning_curves.values, tuning_curves.values))
    tmp = gaussian_filter1d(tmp, sigma=sigma, axis=0)

    return pd.DataFrame(index = tuning_curves.index,
        data = tmp[tuning_curves.shape[0]:tuning_curves.shape[0]*2], 
        columns = tuning_curves.columns
        )

Therefore, we have:

smoothcurves = smoothAngularTuningCurves(tuning_curves, sigma=3)

What does this look like? Let's plot the tuning curves!

plt.figure(figsize=(12, 9))
for i, n in enumerate(pref_ang.sort_values().index.values):
    plt.subplot(8, 4, i + 1, projection='polar')  # Plot the curves in 8 rows and 4 columns
    plt.plot(
        smoothcurves[n], color=color.loc[n]
    )  # Colour of the curves determined by preferred angle    
    plt.xlabel("Angle (rad)")  # Angle in radian, on the X-axis
    plt.ylabel("Firing Rate (Hz)")  # Firing rate in Hz, on the Y-axis
    plt.xticks([])
plt.show()

tutorial HD dataset

Awesome!


Decoding

Now that we have HD tuning curves, we can go one step further. Using only the population activity of ADn units, we can decode the direction the animal is looking in. We will then compare this to the real head direction of the animal, and discover that population activity in the ADn indeed codes for HD.

To decode the population activity, we will be using a Bayesian Decoder as implemented in Pynapple. Just a single line of code!

decoded, proba_feature = nap.decode_1d(
    tuning_curves=tuning_curves,
    group=spikes_adn,
    ep=epochs["wake"],
    bin_size=0.1,  # second
    feature=angle,
)

What does this look like ?

print(decoded)

Out:

Time (s)
---------------  ---------
8812.35          0.772523
8812.45          0.66952
8812.55          0.463514
8812.65          0.66952
8812.75          0.66952
8812.85          0.66952
8812.95          0.566517
8813.05          0.360511
8813.15          0.66952
8813.25          0.463514
8813.35          1.08153
8813.45          0.257508
8813.55          5.92267
8813.65          5.92267
8813.75          5.71667
8813.85          5.92267
8813.95          5.92267
8814.05          6.02568
8814.15          6.23168
8814.25          5.51066
8814.35          5.09865
8814.45          5.40766
8814.55          5.20165
8814.65          5.30466
8814.75          5.09865
8814.85          5.09865
8814.95          5.40766
8815.05          5.71667
8815.15          5.40766
8815.25          5.92267
...
10768.350000007  4.78964
10768.450000007  4.99565
10768.550000007  5.30466
10768.650000007  5.51066
10768.750000007  5.51066
10768.850000007  5.92267
10768.950000007  5.92267
10769.050000007  6.23168
10769.150000007  6.23168
10769.250000007  6.23168
10769.350000007  6.23168
10769.450000007  6.02568
10769.550000007  6.23168
10769.650000007  5.92267
10769.750000007  6.23168
10769.850000007  5.92267
10769.950000007  6.23168
10770.050000007  5.92267
10770.150000007  6.02568
10770.250000007  6.23168
10770.350000007  0.0515015
10770.450000007  6.23168
10770.550000007  0.0515015
10770.650000007  0.0515015
10770.750000007  5.92267
10770.850000007  5.71667
10770.950000007  5.92267
10771.050000007  5.92267
10771.150000007  5.92267
10771.250000007  5.92267
dtype: float64, shape: (19590,)

The variable 'decoded' indicates the most probable angle in which the animal was looking. There is another variable, 'proba_feature' that denotes the probability of a given angular bin at a given time point. We can look at it below:

print(proba_feature.as_dataframe())

Out:

              0.051502  0.154505  ...      6.128681      6.231684
8812.35   2.199077e-06  0.000223  ...  1.483408e-14  2.212419e-11
8812.45   8.561129e-08  0.000013  ...  1.628816e-16  2.464448e-13
8812.55   4.168300e-04  0.022715  ...  2.698994e-09  4.014847e-07
8812.65   1.082000e-05  0.000156  ...  6.175847e-13  4.426551e-10
8812.75   4.128198e-05  0.001369  ...  5.077673e-12  5.290852e-09
...                ...       ...  ...           ...           ...
10770.85  6.695624e-05  0.000003  ...  3.310523e-02  4.842028e-03
10770.95  2.924858e-04  0.000005  ...  5.668237e-02  8.429181e-03
10771.05  1.093979e-03  0.000115  ...  6.114471e-02  2.121581e-02
10771.15  5.537065e-03  0.001235  ...  1.547459e-01  1.115693e-01
10771.25  5.969857e-04  0.000058  ...  6.686085e-02  1.501718e-02

[19590 rows x 61 columns]

Each row of this pandas DataFrame is a time bin, and each column is an angular bin. The sum of all values in a row add up to 1.

Now, let's plot the raster plot for a given period of time, and overlay the actual and decoded HD on the population activity.

ep = nap.IntervalSet(
    start=10717, end=10730
)  # Select an arbitrary interval for plotting

plt.figure()
plt.rc("font", size=12)
for i, n in enumerate(spikes_adn.keys()):
    plt.plot(
        spikes[n].restrict(ep).fillna(pref_ang[n]), "|", color=color.loc[n]
    )  # raster plot for each cell
plt.plot(
    decoded.restrict(ep), "--", color="grey", linewidth=2, label="decoded HD"
)  # decoded HD
plt.legend(loc="upper left")

tutorial HD dataset

Out:

<matplotlib.legend.Legend object at 0x7fc40a176560>

From this plot, we can see that the decoder is able to estimate the head-direction based on the population activity in ADn. Amazing!

What does the probability distribution in this example event look like? Ideally, the bins with the highest probability will correspond to the bins having the most spikes. Let's plot the probability matrix to visualize this.

smoothed = scipy.ndimage.gaussian_filter(
    proba_feature, 1
)  # Smoothening the probability distribution

# Create a DataFrame with the smoothed distribution
p_feature = pd.DataFrame(
    index=proba_feature.index.values,
    columns=proba_feature.columns.values,
    data=smoothed,
)
p_feature = nap.TsdFrame(p_feature)  # Make it a Pynapple TsdFrame

plt.figure()
plt.plot(
    angle.restrict(ep), "w", linewidth=2, label="actual HD", zorder=1
)  # Actual HD, in white
plt.plot(
    decoded.restrict(ep), "--", color="grey", linewidth=2, label="decoded HD", zorder=1
)  # Decoded HD, in grey

# Plot the smoothed probability distribution
plt.imshow(
    np.transpose(p_feature.restrict(ep).values),
    aspect="auto",
    interpolation="bilinear",
    extent=[ep["start"][0], ep["end"][0], 0, 2 * np.pi],
    origin="lower",
    cmap="viridis",
)

plt.xlabel("Time (s)")  # X-axis is time in seconds
plt.ylabel("Angle (rad)")  # Y-axis is the angle in radian
plt.colorbar(label="probability")

tutorial HD dataset

Out:

<matplotlib.colorbar.Colorbar object at 0x7fc40a4eb6d0>

From this probability distribution, we observe that the decoded HD very closely matches the actual HD. Therefore, the population activity in ADn is a reliable estimate of the heading direction of the animal.

I hope this tutorial was helpful. If you have any questions, comments or suggestions, please feel free to reach out to the Pynapple Team!

Total running time of the script: ( 0 minutes 2.354 seconds)

Download Python source code: tutorial_HD_dataset.py

Download Jupyter notebook: tutorial_HD_dataset.ipynb

Gallery generated by mkdocs-gallery