Skip to content

Note

Click here to download the full example code

Advanced processing

The pynapple package provides a small set of high-level functions that are widely used in systems neuroscience.

  • Discrete correlograms
  • Tuning curves
  • Decoding
  • PETH
  • Randomization

This notebook provides few examples with artificial data.

Warning

This tutorial uses seaborn and matplotlib for displaying the figure.

You can install both with pip install matplotlib seaborn

import numpy as np
import pandas as pd
import pynapple as nap
import matplotlib.pyplot as plt
import seaborn as sns

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

Discrete correlograms

First let's generate some data. Here we have two neurons recorded together. We can group them in a TsGroup.

ts1 = nap.Ts(t=np.sort(np.random.uniform(0, 1000, 2000)), time_units="s")
ts2 = nap.Ts(t=np.sort(np.random.uniform(0, 1000, 1000)), time_units="s")
epoch = nap.IntervalSet(start=0, end=1000, time_units="s")
ts_group = nap.TsGroup({0: ts1, 1: ts2}, time_support=epoch)

print(ts_group)

Out:

  Index    rate
-------  ------
      0       2
      1       1

First we can compute their autocorrelograms meaning the number of spikes of a neuron observed in a time windows centered around its own spikes. For this we can use the function compute_autocorrelogram. We need to specifiy the binsize and windowsize to bin the spike train.

autocorrs = nap.compute_autocorrelogram(
    group=ts_group, binsize=100, windowsize=1000, time_units="ms", ep=epoch  # ms
)
print(autocorrs, "\n")

Out:

           0     1
-0.9  0.9475  1.05
-0.8  0.9750  1.10
-0.7  0.9725  0.97
-0.6  1.0150  1.05
-0.5  0.9425  0.85
-0.4  0.9575  0.89
-0.3  0.9425  0.79
-0.2  1.0250  1.11
-0.1  1.0750  0.96
 0.0  0.0000  0.00
 0.1  1.0750  0.96
 0.2  1.0250  1.11
 0.3  0.9425  0.79
 0.4  0.9575  0.89
 0.5  0.9425  0.85
 0.6  1.0150  1.05
 0.7  0.9725  0.97
 0.8  0.9750  1.10
 0.9  0.9475  1.05 

The variable autocorrs is a pandas DataFrame with the center of the bins for the index and each columns is a neuron.

Similarly, we can compute crosscorrelograms meaning how many spikes of neuron 1 do I observe whenever neuron 0 fires. Here the function is called compute_crosscorrelogram and takes a TsGroup as well.

crosscorrs = nap.compute_crosscorrelogram(
    group=ts_group, binsize=100, windowsize=1000, time_units="ms"  # ms
)

print(crosscorrs, "\n")

Out:

          0
          1
-0.9  1.040
-0.8  1.110
-0.7  1.000
-0.6  1.005
-0.5  1.090
-0.4  0.875
-0.3  0.980
-0.2  1.120
-0.1  1.055
 0.0  1.045
 0.1  1.010
 0.2  1.040
 0.3  1.070
 0.4  1.045
 0.5  0.985
 0.6  0.965
 0.7  1.055
 0.8  1.170
 0.9  0.980 

Column name (0, 1) is read as cross-correlogram of neuron 0 and 1 with neuron 0 being the reference time.


Peri-Event Time Histogram (PETH)

A second way to examine the relationship between spiking and an event (i.e. stimulus) is to compute a PETH. pynapple uses the function compute_perievent to center spike time around the timestamps of an event within a given window.

stim = nap.Tsd(
    t=np.sort(np.random.uniform(0, 1000, 50)), d=np.random.rand(50), time_units="s"
)

peth0 = nap.compute_perievent(ts1, stim, minmax=(-0.1, 0.2), time_unit="s")

print(peth0)

Out:

  Index       rate    ref_times
-------  ---------  -----------
      0  nan            23.9996
      1    3.33333      28.0529
      2  nan            46.6504
      3    3.33333      72.9137
      4  nan            73.5197
      5    6.66667      80.8389
      6  nan           117.547
      7  nan           141.686
      8  nan           154.962
      9  nan           160.518
     10  nan           190.095
     11    6.66667     219.417
     12    3.33333     228.274
     13  nan           241.984
     14    6.66667     264.217
     15    3.33333     299.341
     16  nan           316.922
     17  nan           331.141
     18  nan           406.893
     19  nan           422.294
     20  nan           430.157
     21    3.33333     437.542
     22    3.33333     464.405
     23  nan           482.52
     24  nan           487.688
     25    6.66667     494.404
     26  nan           507.024
     27    3.33333     531.726
     28    3.33333     556.851
     29    3.33333     559.877
     30  nan           571.838
     31  nan           647.292
     32    6.66667     709.389
     33    3.33333     809.848
     34  nan           810.405
     35    3.33333     817.995
     36  nan           866.084
     37  nan           886.307
     38  nan           894.487
     39    3.33333     896.847
     40    3.33333     897.95
     41    6.66667     928.571
     42  nan           929.996
     43  nan           933.221
     44    3.33333     933.409
     45    3.33333     938.691
     46  nan           962.453
     47    3.33333     979.04
     48    6.66667     979.768
     49    3.33333     998.711

It is then easy to create a raster plot around the times of the stimulation event by calling the to_tsd function of pynapple to "flatten" the TsGroup peth0.

mkdocs_gallery_thumbnail_number = 2

plt.figure(figsize=(10, 6))
plt.subplot(211)
plt.plot(np.sum(peth0.count(0.01), 1), linewidth=3, color="red")
plt.xlim(-0.1, 0.2)
plt.ylabel("Count")
plt.axvline(0.0)
plt.subplot(212)
plt.plot(peth0.to_tsd(), "|", markersize=20, color="red", mew=4)
plt.xlabel("Time from stim (s)")
plt.ylabel("Stimulus")
plt.xlim(-0.1, 0.2)
plt.axvline(0.0)

tutorial pynapple process

Out:

<matplotlib.lines.Line2D object at 0x7fc40ac70eb0>

The same function can be applied to a group of neurons. In this case, it returns a dict of TsGroup

pethall = nap.compute_perievent(ts_group, stim, minmax=(-0.1, 0.2), time_unit="s")

print(pethall[1])

Out:

  Index       rate    ref_times
-------  ---------  -----------
      0  nan            23.9996
      1  nan            28.0529
      2  nan            46.6504
      3  nan            72.9137
      4  nan            73.5197
      5  nan            80.8389
      6  nan           117.547
      7    3.33333     141.686
      8    6.66667     154.962
      9  nan           160.518
     10  nan           190.095
     11    3.33333     219.417
     12  nan           228.274
     13  nan           241.984
     14    3.33333     264.217
     15  nan           299.341
     16  nan           316.922
     17    3.33333     331.141
     18  nan           406.893
     19    3.33333     422.294
     20  nan           430.157
     21  nan           437.542
     22  nan           464.405
     23  nan           482.52
     24  nan           487.688
     25  nan           494.404
     26  nan           507.024
     27  nan           531.726
     28  nan           556.851
     29  nan           559.877
     30  nan           571.838
     31  nan           647.292
     32  nan           709.389
     33  nan           809.848
     34    3.33333     810.405
     35  nan           817.995
     36  nan           866.084
     37  nan           886.307
     38    3.33333     894.487
     39  nan           896.847
     40  nan           897.95
     41    3.33333     928.571
     42  nan           929.996
     43  nan           933.221
     44  nan           933.409
     45  nan           938.691
     46    3.33333     962.453
     47    3.33333     979.04
     48  nan           979.768
     49    3.33333     998.711

Tuning curves

pynapple can compute 1 dimension tuning curves (for example firing rate as a function of angular direction) and 2 dimension tuning curves ( for example firing rate as a function of position). In both cases, a TsGroup object can be directly passed to the function.

First we will create the 2D features:

dt = 0.1
features = np.vstack((np.cos(np.arange(0, 1000, dt)), np.sin(np.arange(0, 1000, dt)))).T
# features += np.random.randn(features.shape[0], features.shape[1])*0.05
features = nap.TsdFrame(
    t=np.arange(0, 1000, dt),
    d=features,
    time_units="s",
    time_support=epoch,
    columns=["a", "b"],
)

print(features)

plt.figure(figsize=(15, 7))
plt.subplot(121)
plt.plot(features[0:100])
plt.title("Features")
plt.xlabel("Time(s)")
plt.subplot(122)
plt.title("Features")
plt.plot(features["a"][0:100], features["b"][0:100])
plt.xlabel("Feature a")
plt.ylabel("Feature b")

Features, Features

Out:

Time (s)           a         b
----------  --------  --------
0.0          1         0
0.1          0.995     0.09983
0.2          0.98007   0.19867
0.3          0.95534   0.29552
0.4          0.92106   0.38942
0.5          0.87758   0.47943
0.6          0.82534   0.56464
0.7          0.76484   0.64422
0.8          0.69671   0.71736
0.9          0.62161   0.78333
1.0          0.5403    0.84147
1.1          0.4536    0.89121
1.2          0.36236   0.93204
1.3          0.2675    0.96356
1.4          0.16997   0.98545
1.5          0.07074   0.99749
1.6         -0.0292    0.99957
1.7         -0.12884   0.99166
1.8         -0.2272    0.97385
1.9         -0.32329   0.9463
2.0         -0.41615   0.9093
2.1         -0.50485   0.86321
2.2         -0.5885    0.8085
2.3         -0.66628   0.74571
2.4         -0.73739   0.67546
2.5         -0.80114   0.59847
2.6         -0.85689   0.5155
2.7         -0.90407   0.42738
2.8         -0.94222   0.33499
2.9         -0.97096   0.23925
...
997.0       -0.44006  -0.89797
997.1       -0.34822  -0.93741
997.2       -0.25289  -0.96749
997.3       -0.15504  -0.98791
997.4       -0.05564  -0.99845
997.5        0.04432  -0.99902
997.6        0.14383  -0.9896
997.7        0.24191  -0.9703
997.8        0.33757  -0.9413
997.9        0.42986  -0.9029
998.0        0.51785  -0.85547
998.1        0.60066  -0.7995
998.2        0.67748  -0.73554
998.3        0.74753  -0.66423
998.4        0.81011  -0.58628
998.5        0.86459  -0.50248
998.6        0.91043  -0.41365
998.7        0.94718  -0.3207
998.8        0.97447  -0.22453
998.9        0.99201  -0.12613
999.0        0.99965  -0.02646
999.1        0.9973    0.07347
999.2        0.98498   0.17267
999.3        0.96282   0.27014
999.4        0.93104   0.36491
999.5        0.88996   0.45604
999.6        0.83999   0.54261
999.7        0.78162   0.62375
999.8        0.71544   0.69867
999.9        0.64212   0.7666
dtype: float64, shape: (10000, 2)

Text(732.5909090909089, 0.5, 'Feature b')

Here we call the function compute_2d_tuning_curves. To check the accuracy of the tuning curves, we will display the spikes aligned to the features with the function value_from which assign to each spikes the corresponding feature value for neuron 0.

tcurves2d, binsxy = nap.compute_2d_tuning_curves(
    group=ts_group, features=features, nb_bins=10
)

ts_to_features = ts_group[1].value_from(features)

plt.figure()
plt.plot(ts_to_features["a"], ts_to_features["b"], "o", color="red", markersize=4)
extents = (
    np.min(features["b"]),
    np.max(features["b"]),
    np.min(features["a"]),
    np.max(features["a"]),
)
plt.imshow(tcurves2d[1].T, origin="lower", extent=extents, cmap="viridis")
plt.title("Tuning curve unit 0 2d")
plt.xlabel("feature a")
plt.ylabel("feature b")
plt.grid(False)
plt.show()

Tuning curve unit 0 2d

Out:

/mnt/home/gviejo/pynapple/pynapple/process/tuning_curves.py:223: RuntimeWarning: invalid value encountered in divide
  count = count / occupancy

Decoding

Pynapple supports 1 dimensional and 2 dimensional bayesian decoding. The function returns the decoded feature as well as the probabilities for each timestamps.

First we generate some artificial "place fields" in 2 dimensions based on the features.

This part is just to generate units with a relationship to the features (i.e. "place fields")

times = features.as_units("us").index.values
ft = features.values
alpha = np.arctan2(ft[:, 1], ft[:, 0])
bins = np.repeat(np.linspace(-np.pi, np.pi, 13)[::, np.newaxis], 2, 1)
bins += np.array([-2 * np.pi / 24, 2 * np.pi / 24])
ts_group = {}
for i in range(12):
    ts = times[(alpha >= bins[i, 0]) & (alpha <= bins[i + 1, 1])]
    ts_group[i] = nap.Ts(ts, time_units="us")

ts_group = nap.TsGroup(ts_group, time_support=epoch)
print(ts_group)

Out:

  Index    rate
-------  ------
      0   1.248
      1   1.665
      2   1.666
      3   1.664
      4   1.665
      5   1.67
      6   1.671
      7   1.673
      8   1.665
      9   1.666
     10   1.667
     11   1.248

To decode we need to compute tuning curves in 2D.

import warnings

warnings.filterwarnings("ignore")

tcurves2d, binsxy = nap.compute_2d_tuning_curves(
    group=ts_group,
    features=features,
    nb_bins=10,
    ep=epoch,
    minmax=(-1.0, 1.0, -1.0, 1.0),
)

Then we plot the "place fields".

plt.figure(figsize=(20, 9))
for i in ts_group.keys():
    plt.subplot(2, 6, i + 1)
    plt.imshow(
        tcurves2d[i], extent=(binsxy[1][0], binsxy[1][-1], binsxy[0][0], binsxy[0][-1])
    )
    plt.xticks()
plt.show()

tutorial pynapple process

Then we call the actual decoding function in 2d.

decoded, proba_feature = nap.decode_2d(
    tuning_curves=tcurves2d,
    group=ts_group,
    ep=epoch,
    bin_size=0.1,  # second
    xy=binsxy,
    features=features,
)


plt.figure(figsize=(15, 5))
plt.subplot(131)
plt.plot(features["a"].as_units("s").loc[0:20], label="True")
plt.plot(decoded["a"].as_units("s").loc[0:20], label="Decoded")
plt.legend()
plt.xlabel("Time (s)")
plt.ylabel("Feature a")
plt.subplot(132)
plt.plot(features["b"].as_units("s").loc[0:20], label="True")
plt.plot(decoded["b"].as_units("s").loc[0:20], label="Decoded")
plt.legend()
plt.xlabel("Time (s)")
plt.ylabel("Feature b")
plt.subplot(133)
plt.plot(
    features["a"].as_units("s").loc[0:20],
    features["b"].as_units("s").loc[0:20],
    label="True",
)
plt.plot(
    decoded["a"].as_units("s").loc[0:20],
    decoded["b"].as_units("s").loc[0:20],
    label="Decoded",
)
plt.xlabel("Feature a")
plt.ylabel("Feature b")
plt.legend()
plt.tight_layout()
plt.show()

tutorial pynapple process


Randomization

Pynapple provides some ready-to-use randomization methods to compute null distributions for statistical testing. Different methods preserve or destroy different features of the data, here's a brief overview.

shift_timestamps shifts all the timestamps in a Ts object by the same random amount, wrapping the end of the time support to its beginning. This randomization preserves the temporal structure in the data but destroys the temporal relationships with other quantities (e.g. behavioural data). When applied on a TsGroup object, each series in the group is shifted independently.

ts = nap.Ts(t=np.sort(np.random.uniform(0, 100, 10)), time_units="ms")
rand_ts = nap.shift_timestamps(ts, min_shift=1, max_shift=20)

shuffle_ts_intervals computes the intervals between consecutive timestamps, permutes them, and generates a new set of timestamps with the permuted intervals. This procedure preserve the distribution of intervals, but not their sequence.

ts = nap.Ts(t=np.sort(np.random.uniform(0, 100, 10)), time_units="s")
rand_ts = nap.shuffle_ts_intervals(ts)

jitter_timestamps shifts each timestamp in the data of an independent random amount. When applied with a small max_jitter, this procedure destroys the fine temporal structure of the data, while preserving structure on longer timescales.

ts = nap.Ts(t=np.sort(np.random.uniform(0, 100, 10)), time_units="s")
rand_ts = nap.jitter_timestamps(ts, max_jitter=1)

resample_timestamps uniformly re-draws the same number of timestamps in ts, in the same time support. This procedures preserve the total number of timestamps, but destroys any other feature of the original data.

ts = nap.Ts(t=np.sort(np.random.uniform(0, 100, 10)), time_units="s")
rand_ts = nap.resample_timestamps(ts)

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

Download Python source code: tutorial_pynapple_process.py

Download Jupyter notebook: tutorial_pynapple_process.ipynb

Gallery generated by mkdocs-gallery