Introduction to pynapple#

The goal of this tutorial is to quickly learn enough about pynapple to get started with data analysis. This tutorial assumes familiarity with the basics functionalities of numpy.

You can check how to install pynapple here.

Important

By default, pynapple will assume a time units in seconds when passing timestamps array or time parameters such as bin size (unless specified with the time_units argument)


Importing pynapple#

The convention is to import pynapple with a namespace:

import pynapple as nap
Hide code cell content
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
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)

Instantiating pynapple objects#

nap.Tsd: 1-dimensional time serie#

If you have a 1-dimensional time series, you use the nap.Tsd object. The arguments t and d are the arguments for timestamps and data.

tsd = nap.Tsd(t=np.arange(100), d=np.random.rand(100))

print(tsd)
Time (s)
----------  ---------
0.0         0.0419045
1.0         0.28696
2.0         0.445331
3.0         0.25286
4.0         0.977419
5.0         0.654099
6.0         0.0982688
...
93.0        0.156056
94.0        0.655023
95.0        0.71061
96.0        0.712324
97.0        0.756803
98.0        0.814128
99.0        0.216317
dtype: float64, shape: (100,)

nap.TsdFrame: 2-dimensional time series#

If you have a 2-dimensional time series, you use the nap.TsdFrame object. The arguments t and d are the arguments for timestamps and data. You can add the argument columns to label each columns.

tsdframe = nap.TsdFrame(
    t=np.arange(100), d=np.random.rand(100, 3), columns=["a", "b", "c"]
)

print(tsdframe)
Time (s)          a        b        c
----------  -------  -------  -------
0.0         0.68463  0.48899  0.59473
1.0         0.66339  0.54058  0.16374
2.0         0.52071  0.97668  0.14466
3.0         0.95294  0.52125  0.0019
4.0         0.75873  0.2345   0.26705
5.0         0.95877  0.33824  0.7019
6.0         0.455    0.92523  0.57711
...
93.0        0.12078  0.2506   0.01804
94.0        0.94825  0.55719  0.03262
95.0        0.76913  0.16407  0.81356
96.0        0.60164  0.26907  0.0516
97.0        0.04448  0.31542  0.21865
98.0        0.40719  0.6116   0.70531
99.0        0.38447  0.00357  0.86669
dtype: float64, shape: (100, 3)

nap.TsdTensor: n-dimensionals time series#

If you have larger than 2 dimensions time series (typically movies), you use the nap.TsdTensor object . The arguments t and d are the arguments for timestamps and data.

tsdtensor = nap.TsdTensor(
    t=np.arange(100), d=np.random.rand(100, 3, 4)
)

print(tsdtensor)
Time (s)
----------  -----------------------------
0.0         [[0.399939 ... 0.665792] ...]
1.0         [[0.253245 ... 0.034021] ...]
2.0         [[0.706434 ... 0.736635] ...]
3.0         [[0.977234 ... 0.74833 ] ...]
4.0         [[0.074715 ... 0.703523] ...]
5.0         [[0.334406 ... 0.623211] ...]
6.0         [[0.194367 ... 0.474491] ...]
...
93.0        [[0.63976  ... 0.127476] ...]
94.0        [[0.840731 ... 0.549079] ...]
95.0        [[0.827007 ... 0.813617] ...]
96.0        [[0.867786 ... 0.477116] ...]
97.0        [[0.123354 ... 0.962512] ...]
98.0        [[0.965874 ... 0.660707] ...]
99.0        [[0.610749 ... 0.217282] ...]
dtype: float64, shape: (100, 3, 4)

nap.IntervalSet: intervals#

The IntervalSet object stores multiple epochs with a common time unit in a table format. The epochs are strictly non-overlapping. Both start and end arguments are necessary.

epochs = nap.IntervalSet(start=[0, 10], end=[5, 15])

print(epochs)
            start    end
       0        0      5
       1       10     15
shape: (2, 2), time unit: sec.

nap.Ts: timestamps#

Ts object stores timestamps data (typically spike times or reward times). The argument t for timestamps is necessary.

ts = nap.Ts(t=np.sort(np.random.uniform(0, 100, 10)))

print(ts)
Time (s)
21.974110326
31.934023239
38.968793995
39.313021866
41.116480522
50.037935186
63.609101828
64.531953121
75.609469709
77.932122498
shape: 10

nap.TsGroup: group of timestamps#

TsGroup is a dictionnary that stores multiple time series with different time stamps (.i.e. a group of neurons with different spike times from one session). The first argument data can be a dictionnary of Ts, Tsd or numpy 1d array.

data = {
    0: nap.Ts(t=np.sort(np.random.uniform(0, 100, 1000))),
    1: nap.Ts(t=np.sort(np.random.uniform(0, 100, 2000))),
    2: nap.Ts(t=np.sort(np.random.uniform(0, 100, 3000))),
}

tsgroup = nap.TsGroup(data)

print(tsgroup, "\n")
  Index     rate
-------  -------
      0  10.0015
      1  20.003
      2  30.0045 

Interaction between pynapple objects#

Time support : attribute of time series#

A key feature of how pynapple manipulates time series is an inherent time support object defined for Ts, Tsd, TsdFrame and TsGroup objects. The time support object is defined as an IntervalSet that provides the time serie with a context. For example, the restrict operation will automatically update the time support object for the new time series. Ideally, the time support object should be defined for all time series when instantiating them. If no time series is given, the time support is inferred from the start and end of the time series.

In this example, a Tsd is instantiated with and without a time support of intervals 0 to 5 seconds. Notice how the shape of the Tsd varies.

time_support = nap.IntervalSet(start=0, end=2)

print(time_support)
            start    end
       0        0      2
shape: (1, 2), time unit: sec.

Without time support :

print(nap.Tsd(t=[0, 1, 2, 3, 4], d=[0, 1, 2, 3, 4]))
Time (s)
----------  --
0            0
1            1
2            2
3            3
4            4
dtype: int64, shape: (5,)

With time support :

print(
    nap.Tsd(
        t=[0, 1, 2, 3, 4], d=[0, 1, 2, 3, 4], 
        time_support = time_support
        )
    )
Time (s)
----------  --
0            0
1            1
2            2
dtype: int64, shape: (3,)

The time support object has become an attribute of the time series. Depending on the operation applied to the time series, it will be updated.

tsd = nap.Tsd(
    t=np.arange(10), d=np.random.randn(10), 
    time_support = time_support
    )

print(tsd.time_support)
            start    end
       0        0      2
shape: (1, 2), time unit: sec.

Restricting time series to epochs#

The central function of pynapple is the restrict method of Ts, Tsd, TsdFrame and TsGroup. The argument is an IntervalSet object. Only time points within the intervals of the IntervalSet object are returned. The time support of the time series is updated accordingly.

tsd = nap.Tsd(t=np.arange(10), d=np.random.randn(10))

ep = nap.IntervalSet(start=[0, 7], end=[3.5, 12.4])

print(ep)
            start    end
       0        0    3.5
       1        7   12.4
shape: (2, 2), time unit: sec.

From :

print(tsd)
Time (s)
----------  -----------
0            0.112951
1           -0.588607
2           -0.688425
3            1.85012
4            0.669596
5           -0.00482399
6            1.49329
7            0.19652
8            0.683482
9            0.202721
dtype: float64, shape: (10,)

to :

new_tsd = tsd.restrict(ep)

print(new_tsd)
Time (s)
----------  ---------
0            0.112951
1           -0.588607
2           -0.688425
3            1.85012
7            0.19652
8            0.683482
9            0.202721
dtype: float64, shape: (7,)

Numpy & pynapple#

Pynapple relies on numpy to store the data. Pynapple objects behave very similarly to numpy and numpy functions can be applied directly

tsdtensor = nap.TsdTensor(t=np.arange(100), d=np.random.rand(100, 3, 4))

If a numpy function preserves the time axis, a pynapple object is returned.

In this example, averaging a TsdTensor along the second dimension returns a TsdFrame:

print(
    np.mean(tsdtensor, 1)
    )
Time (s)          0        1        2        3
----------  -------  -------  -------  -------
0.0         0.55515  0.66206  0.93337  0.54385
1.0         0.68673  0.10987  0.8123   0.52544
2.0         0.45099  0.43491  0.4675   0.17394
3.0         0.70168  0.30065  0.36481  0.16647
4.0         0.31712  0.46112  0.38566  0.67702
5.0         0.61942  0.54528  0.32383  0.48176
6.0         0.2482   0.5433   0.78477  0.15299
...
93.0        0.65609  0.33773  0.29112  0.83802
94.0        0.50509  0.56352  0.1848   0.26951
95.0        0.37588  0.4126   0.7674   0.69086
96.0        0.43349  0.14707  0.54091  0.46967
97.0        0.5626   0.29892  0.49404  0.3183
98.0        0.38963  0.47193  0.38748  0.82124
99.0        0.27964  0.35374  0.1979   0.27775
dtype: float64, shape: (100, 4)

Averaging along the time axis will return a numpy array object:

print(
    np.mean(tsdtensor, 0)
    )
[[0.47757443 0.43653194 0.47963126 0.55119309]
 [0.49293072 0.49890985 0.50761137 0.54307044]
 [0.4492723  0.48052291 0.48113408 0.49867939]]

Slicing objects#

Slicing time series and intervals#

Ts, Tsd, TsdFrame, TsdTensor and IntervalSet can be sliced similar to numpy array:

tsdframe = nap.TsdFrame(t=np.arange(10)/10, d=np.random.randn(10,4))
print(tsdframe)
Time (s)           0         1         2         3
----------  --------  --------  --------  --------
0            0.19346   2.00807  -0.68304  -0.82794
0.1          0.6998   -0.21982  -1.09566  -0.03358
0.2          1.58193   0.62368   0.31561  -0.46195
0.3         -0.24569   1.3821   -0.13477   0.6488
0.4          0.56566   0.27042   0.69332   1.41362
0.5         -0.86512   0.64391   0.3285    1.29234
0.6          0.09494   1.77775   1.52961   0.62798
0.7          0.6737    0.52707   0.78292   1.62389
0.8         -0.31884  -0.23121   0.45202  -0.11377
0.9          1.11153  -0.43061  -0.23533   0.73301
dtype: float64, shape: (10, 4)
print(tsdframe[4:7])
Time (s)           0        1        2        3
----------  --------  -------  -------  -------
0.4          0.56566  0.27042  0.69332  1.41362
0.5         -0.86512  0.64391  0.3285   1.29234
0.6          0.09494  1.77775  1.52961  0.62798
dtype: float64, shape: (3, 4)
print(tsdframe[:,0])
Time (s)
----------  ----------
0            0.193462
0.1          0.699796
0.2          1.58193
0.3         -0.245686
0.4          0.565659
0.5         -0.865117
0.6          0.0949378
0.7          0.673705
0.8         -0.318841
0.9          1.11153
dtype: float64, shape: (10,)
ep = nap.IntervalSet(start=[0, 10, 20], end=[4, 15, 25])
print(ep)
            start    end
       0        0      4
       1       10     15
       2       20     25
shape: (3, 2), time unit: sec.
print(ep[0:2])
            start    end
       0        0      4
       1       10     15
shape: (2, 2), time unit: sec.
print(ep[1])
            start    end
       0       10     15
shape: (1, 2), time unit: sec.

Slicing TsGroup#

TsGroup object can be indexed to return directly the timestamp object or sliced to return a new TsGroup.

Indexing :

print(tsgroup[0], "\n")
Time (s)
0.006121428
0.026009191
0.079232261
0.221315297
0.249503344
0.345913183
0.440613522
...
98.559852929
98.697597025
98.797881505
98.958378919
98.969810071
99.031695619
99.573531764
shape: 1000 

Slicing :

print(tsgroup[[0, 2]])
  Index     rate
-------  -------
      0  10.0015
      2  30.0045

Core functions#

Objects have methods that can help transform and refine time series. This is a non exhaustive list.

Binning : counting events#

Time series objects have the count method that count the number of timestamps. This is typically used when counting number of spikes within a particular bin over multiple intervals. The returned object is a Tsd or TsdFrame with the timestamps being the center of the bins.

count = tsgroup.count(1)

print(count)
Time (s)        0    1    2
------------  ---  ---  ---
0.506121428    11   31   28
1.506121428    10    9   26
2.506121428     6   17   39
3.506121428     8   12   22
4.506121428    17    8   31
5.506121428     7   21   31
6.506121428     9   30   36
...
93.506121428    8   17   28
94.506121428   11   21   27
95.506121428   14   26   32
96.506121428   12   21   28
97.506121428    9   20   18
98.506121428   10   21   31
99.506121428    2   28   34
dtype: float64, shape: (100, 3)

Thresholding#

Some time series have specific methods. The threshold method of Tsd returns a new Tsd with all the data above or below a given value.

tsd = nap.Tsd(t=np.arange(10), d=np.random.rand(10))

print(tsd)

print(tsd.threshold(0.5))
Time (s)
----------  ---------
0           0.611334
1           0.51313
2           0.201237
3           0.80765
4           0.782311
5           0.349644
6           0.0573633
7           0.273562
8           0.964082
9           0.481031
dtype: float64, shape: (10,)
Time (s)
----------  --------
0           0.611334
1           0.51313
3           0.80765
4           0.782311
8           0.964082
dtype: float64, shape: (5,)

An important aspect of the tresholding is that the time support get updated based on the time points remaining. To get the epochs above/below a certain threshold, you can access the time support of the returned object.

print(tsd.time_support)

print(tsd.threshold(0.5, "below").time_support)
            start    end
       0        0      9
shape: (1, 2), time unit: sec.
            start    end
       0      1.5    2.5
       1      4.5    7.5
       2      8.5    9
shape: (3, 2), time unit: sec.

Time-bin averaging of data#

Many analyses requires to bring time series to the same rates and same dimensions. A quick way to downsample a time series to match in size for example a count array is to bin average. The bin_average method takes a bin size in unit of time.

tsdframe = nap.TsdFrame(t=np.arange(0, 100)/10, d=np.random.randn(100,3))

print(tsdframe)
Time (s)           0         1         2
----------  --------  --------  --------
0.0          0.7961   -0.79039   0.4632
0.1         -0.40168  -1.36247   0.02202
0.2          0.28476   0.22614   1.03102
0.3          0.00107  -0.83518  -0.66076
0.4         -1.74899  -2.90451  -0.14016
0.5          0.03932   2.57255   0.6165
0.6          0.10894   0.9159    0.20869
...
9.3          0.56777   0.08537  -0.62286
9.4         -1.80238   0.77951  -1.05743
9.5         -0.22058  -1.316    -1.35451
9.6         -0.25012  -1.03378   0.74031
9.7         -0.86778   0.48416   0.70793
9.8          0.76119  -0.07983  -0.96462
9.9         -0.50543  -0.34753   0.83886
dtype: float64, shape: (100, 3)

Here we go from a timepoint every 100ms to a timepoint every second.

print(tsdframe.bin_average(1))
Time (s)           0         1         2
----------  --------  --------  --------
0.5          0.22002  -0.2002    0.00094
1.5          0.11907  -0.55017  -0.59469
2.5          0.5543    0.05228  -0.40471
3.5         -0.69956   0.00081   0.69764
4.5          0.3057   -0.44523   0.00515
5.5          0.09229  -0.17765  -0.26089
6.5          0.11155  -0.13456   0.16988
7.5         -0.33502   0.19991  -0.07574
8.5          0.5115   -0.08241   0.41457
9.5         -0.48471  -0.19913  -0.39653
dtype: float64, shape: (10, 3)

Loading data#

See here for more details about loading data.

Loading NWB#

Pynapple supports by default the NWB standard.

NWB files can be loaded with :

nwb = nap.load_file("path/to/my.nwb")

or directly with the NWBFile class:

nwb = nap.NWBFile("path/to/my.nwb")

print(nwb)
my.nwb
┍━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━┑
│ Keys            │ Type        │
┝━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━┥
│ units           │ TsGroup     │
┕━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━┙

The returned object behaves like a dictionnary. The first column indicates the keys. The second column indicate the object type.

print(nwb['units'])
  Index    rate  location      group
-------  ------  ----------  -------
      0    1.0  brain        0
      1    1.0  brain        0
      2    1.0  brain        0

Overview of advanced analysis#

The process module of pynapple contains submodules that group methods that can be applied for high level analysis. All of the method are directly available from the nap namespace.

Important

Some functions have been doubled given the nature of the data. For instance, computing a 1d tuning curves from spiking activity requires the nap.compute_1d_tuning_curves. The same function for calcium imaging data which is a continuous time series is available with nap.compute_1d_tuning_curves_continuous.

Discrete correlograms

This module computes correlograms of discrete events, for example the cross-correlograms of a population of neurons.

Bayesian decoding

The decoding module perfoms bayesian decoding given a set of tuning curves and a TsGroup.

Filtering

Bandpass, lowpass, highpass or bandstop filtering can be done to any time series using either Butterworth filter or windowed-sinc convolution.

Perievent time histogram

The perievent module has a set of functions to center time series and timestamps data around a particular events.

Randomizing

The randomize module holds multiple technique to shuffle timestamps in order to create surrogate datasets.

Spectrum

The spectrum module contains the methods to return the (mean) power spectral density of a time series.

Tuning curves

Tuning curves of neurons based on spiking or calcium activity can be computed.

Wavelets

The wavelets module performs Morlet wavelets decomposition of a time series.