pynapple.process.decoding#

Functions to decode n-dimensional features.

Functions

decode_1d(tuning_curves, group, ep, bin_size)

decode_2d(tuning_curves, group, ep, bin_size, xy)

decode_bayes(tuning_curves, data, epochs, ...)

Performs Bayesian decoding over n-dimensional features.

decode_template(tuning_curves, data, epochs, ...)

Performs template matching decoding over n-dimensional features.

pynapple.process.decoding.decode_1d(tuning_curves, group, ep, bin_size, time_units='s', feature=None)[source]#

Deprecated since version 0.9.2: decode_1d will be removed in Pynapple 1.0.0, it is replaced by decode_bayes because the latter works for N dimensions.

pynapple.process.decoding.decode_2d(tuning_curves, group, ep, bin_size, xy, time_units='s', features=None)[source]#

Deprecated since version 0.9.2: decode_2d will be removed in Pynapple 1.0.0, it is replaced by decode_bayes because the latter works for N dimensions.

pynapple.process.decoding.decode_bayes(tuning_curves, data, epochs, bin_size, sliding_window_size=None, time_units='s', uniform_prior=True)[source]#

Performs Bayesian decoding over n-dimensional features.

The algorithm is based on Bayes’ rule:

\[P(x|n) \propto P(n|x) P(x)\]

where:

  • \(P(x|n)\) is the posterior probability of the feature value given the observed neural activity.

  • \(P(n|x)\) is the likelihood of the neural activity given the feature value.

  • \(P(x)\) is the prior probability of the feature value.

Mapping this to the function:

  • \(P(x|n)\) is the estimated probability distribution over the decoded feature for each time bin. This is the output of the function. The decoded value is the one with the maximum posterior probability.

  • \(P(n|x)\) is determined by the tuning curves. Assuming spikes follow a Poisson distribution and neurons are conditionally independent:

    \[P(n|x) = \prod_{i=1}^{N} P(n_i|x) = \prod_{i=1}^{N} \frac{\lambda_i^{n_i} e^{-\lambda_i}}{n_i!}\]

    where \(\lambda_i\) is the expected firing rate of neuron \(i\) at feature value \(x\), and \(n_i\) is the spike count of neuron \(i\).

  • \(P(x)\) depends on the value of the uniform_prior argument. If uniform_prior=True, it is a uniform distribution over feature values. If uniform_prior=False, it is based on the occupancy (i.e. the time spent in each feature bin during tuning curve estimation).

References

Parameters:
  • tuning_curves (xarray.DataArray) – Tuning curves as computed by compute_tuning_curves().

  • data (TsGroup or TsdFrame) – Neural activity with the same keys as the tuning curves. You may also pass a TsdFrame with smoothed counts.

  • epochs (IntervalSet) – The epochs on which decoding is computed

  • bin_size (float) – Bin size. Default in seconds. Use time_units to change it.

  • sliding_window_size (int, optional) – The size, in number of bins, for a uniform window to be convolved with the counts array for each neuron. Value should be >= 1. If None (default), no smoothing is applied.

  • time_units (str, optional) – Time unit of the bin size (s [default], ms, us).

  • uniform_prior (bool, optional) – If True (default), uses a uniform distribution as a prior. If False, uses the occupancy from the tuning curves as a prior over the feature probability distribution.

Returns:

  • Tsd – The decoded feature.

  • TsdFrame, TsdTensor – The probability distribution of the decoded feature for each time bin.

Examples

In the simplest case, we can decode a single feature (e.g., position) from a group of neurons:

>>> import pynapple as nap
>>> import numpy as np
>>> data = nap.TsGroup({i: nap.Ts(t=np.arange(0, 50) + 50 * i) for i in range(2)})
>>> feature = nap.Tsd(t=np.arange(0, 100, 1), d=np.repeat(np.arange(0, 2), 50))
>>> tuning_curves = nap.compute_tuning_curves(data, feature, bins=2, range=(-.5, 1.5))
>>> epochs = nap.IntervalSet([0, 100])
>>> decoded, p = nap.decode_bayes(tuning_curves, data, epochs=epochs, bin_size=1)
>>> decoded
Time (s)
----------  --
0.5          0
1.5          0
2.5          0
3.5          0
4.5          0
5.5          0
6.5          0
...
93.5         1
94.5         1
95.5         1
96.5         1
97.5         1
98.5         1
99.5         1
dtype: float64, shape: (100,)

decode is a Tsd object containing the decoded feature for each time bin.

>>> p
Time (s)    0.0    1.0
----------  -----  -----
0.5         1.0    0.0
1.5         1.0    0.0
2.5         1.0    0.0
3.5         1.0    0.0
4.5         1.0    0.0
5.5         1.0    0.0
6.5         1.0    0.0
...         ...    ...
93.5        0.0    1.0
94.5        0.0    1.0
95.5        0.0    1.0
96.5        0.0    1.0
97.5        0.0    1.0
98.5        0.0    1.0
99.5        0.0    1.0
dtype: float64, shape: (100, 2)

p is a TsdFrame object containing the probability distribution for each time bin.

The function also works for multiple features, in which case it does n-dimensional decoding:

>>> features = nap.TsdFrame(
...     t=np.arange(0, 100, 1),
...     d=np.vstack((np.repeat(np.arange(0, 2), 50), np.tile(np.arange(0, 2), 50))).T,
... )
>>> data = nap.TsGroup(
...     {
...         0: nap.Ts(np.arange(0, 50, 2)),
...         1: nap.Ts(np.arange(1, 51, 2)),
...         2: nap.Ts(np.arange(50, 100, 2)),
...         3: nap.Ts(np.arange(51, 101, 2)),
...     }
... )
>>> tuning_curves = nap.compute_tuning_curves(data, features, bins=2, range=[(-.5, 1.5)]*2)
>>> decoded, p = nap.decode_bayes(tuning_curves, data, epochs=epochs, bin_size=1)
>>> decoded
Time (s)    0    1
----------  ---  ---
0.5         0.0  0.0
1.5         0.0  1.0
2.5         0.0  0.0
3.5         0.0  1.0
4.5         0.0  0.0
5.5         0.0  1.0
6.5         0.0  0.0
...         ...  ...
93.5        1.0  1.0
94.5        1.0  0.0
95.5        1.0  1.0
96.5        1.0  0.0
97.5        1.0  1.0
98.5        1.0  0.0
99.5        1.0  1.0
dtype: float64, shape: (100, 2)

decoded is now a TsdFrame object containing the decoded features for each time bin.

>>> p
Time (s)
----------  --------------
0.5         [[1., 0.] ...]
1.5         [[0., 1.] ...]
2.5         [[1., 0.] ...]
3.5         [[0., 1.] ...]
4.5         [[1., 0.] ...]
5.5         [[0., 1.] ...]
6.5         [[1., 0.] ...]
...
93.5        [[0., 0.] ...]
94.5        [[0., 0.] ...]
95.5        [[0., 0.] ...]
96.5        [[0., 0.] ...]
97.5        [[0., 0.] ...]
98.5        [[0., 0.] ...]
99.5        [[0., 0.] ...]
dtype: float64, shape: (100, 2, 2)

and p is a TsdTensor object containing the probability distribution for each time bin.

It is also possible to pass continuous values instead of spikes (e.g. smoothed spike counts):

>>> data = data.count(1).smooth(2)
>>> tuning_curves = nap.compute_tuning_curves(data, features, bins=2, range=[(-.5, 1.5)]*2)
>>> decoded, p = nap.decode_bayes(tuning_curves, data, epochs=epochs, bin_size=1)
>>> decoded
Time (s)    0    1
----------  ---  ---
0.5         0.0  1.0
1.5         0.0  1.0
2.5         0.0  1.0
3.5         0.0  1.0
4.5         0.0  0.0
5.5         0.0  0.0
6.5         0.0  0.0
...         ...  ...
92.5        1.0  0.0
93.5        1.0  0.0
94.5        1.0  0.0
95.5        1.0  1.0
96.5        1.0  1.0
97.5        1.0  1.0
98.5        1.0  1.0
dtype: float64, shape: (98, 2)
pynapple.process.decoding.decode_template(tuning_curves, data, epochs, bin_size, metric='correlation', sliding_window_size=None, time_units='s')[source]#

Performs template matching decoding over n-dimensional features.

The algorithm decodes as follow:

\[\hat{x}(t) = \arg\min\limits_{x} [dist(f(x), n(t))]\]

where:

  • \(f(x)\) is the the tuning curve function.

  • \(n(t)\) is input neural activity at time \(t\).

  • \(dist\) is a distance metric.

The algorithm computes the distance between the observed neural activity and the tuning curves for every time bin. The decoded feature at each time bin corresponds to the tuning curve bin with the smallest distance.

See scipy.spatial.distance.cdist() for available distance metrics and how they are computed.

References

Parameters:
  • tuning_curves (xarray.DataArray) – Tuning curves as computed by compute_tuning_curves().

  • data (TsGroup or TsdFrame) – Neural activity with the same keys as the tuning curves. You may also pass a TsdFrame with smoothed counts.

  • epochs (IntervalSet) – The epochs on which decoding is computed

  • bin_size (float) – Bin size. Default is second. Use time_units to change it.

  • metric (str or callable, optional) –

    The distance metric to use for template matching.

    If a string, passed to scipy.spatial.distance.cdist(), must be one of: braycurtis, canberra, chebyshev, cityblock, correlation, cosine, dice, euclidean, hamming, jaccard, jensenshannon, kulczynski1, mahalanobis, matching, minkowski, rogerstanimoto, russellrao, seuclidean, sokalmichener, sokalsneath, sqeuclidean or yule.

    Default is correlation.

    Note

    Some metrics may not be suitable for all types of data. For example, metrics such as hamming do not handle NaN values.

    If a callable, it must have the signature metric(u, v) -> float and return the distance between two 1D arrays.

  • sliding_window_size (int, optional) – The size, in number of bins, for a uniform window to be convolved with the counts array for each neuron. Value should be >= 1. If None (default), no smoothing is applied.

  • time_units (str, optional) – Time unit of the bin size (s [default], ms, us).

Returns:

  • Tsd – The decoded feature

  • TsdFrame or TsdTensor – The distance matrix between the neural activity and the tuning curves for each time bin.

Examples

In the simplest case, we can decode a single feature (e.g., position) from a group of neurons:

>>> import pynapple as nap
>>> import numpy as np
>>> group = nap.TsGroup({i: nap.Ts(t=np.arange(0, 50) + 50 * i) for i in range(2)})
>>> feature = nap.Tsd(t=np.arange(0, 100, 1), d=np.repeat(np.arange(0, 2), 50))
>>> tuning_curves = nap.compute_tuning_curves(group, feature, bins=2, range=(-.5, 1.5))
>>> epochs = nap.IntervalSet([0, 100])
>>> decoded, dist = nap.decode_template(tuning_curves, group, epochs=epochs, bin_size=1)
>>> decoded
Time (s)
----------  --
0.5          0
1.5          0
2.5          0
3.5          0
4.5          0
5.5          0
6.5          0
...
93.5         1
94.5         1
95.5         1
96.5         1
97.5         1
98.5         1
99.5         1
dtype: float64, shape: (100,)

decode is a Tsd object containing the decoded feature for each time bin.

>>> p
Time (s)    0.0    1.0
----------  -----  -----
0.5         0.0    2.0
1.5         0.0    2.0
2.5         0.0    2.0
3.5         0.0    2.0
4.5         0.0    2.0
5.5         0.0    2.0
...         ...    ...
94.5        2.0    0.0
95.5        2.0    0.0
96.5        2.0    0.0
97.5        2.0    0.0
98.5        2.0    0.0
99.5        2.0    0.0
dtype: float64, shape: (100, 2)

dist is a TsdFrame object containing the distances for each time bin.

The function also works for multiple features, in which case it does n-dimensional decoding:

>>> features = nap.TsdFrame(
...     t=np.arange(0, 100, 1),
...     d=np.vstack((np.repeat(np.arange(0, 2), 50), np.tile(np.arange(0, 2), 50))).T,
... )
>>> group = nap.TsGroup(
...     {
...         0: nap.Ts(np.arange(0, 50, 2)),
...         1: nap.Ts(np.arange(1, 51, 2)),
...         2: nap.Ts(np.arange(50, 100, 2)),
...         3: nap.Ts(np.arange(51, 101, 2)),
...     }
... )
>>> tuning_curves = nap.compute_tuning_curves(group, features, bins=2, range=[(-.5, 1.5)]*2)
>>> decoded, dist = nap.decode_template(tuning_curves, group, epochs=epochs, bin_size=1)
>>> decoded
Time (s)    0    1
----------  ---  ---
0.5         0.0  0.0
1.5         0.0  1.0
2.5         0.0  0.0
3.5         0.0  1.0
4.5         0.0  0.0
5.5         0.0  1.0
6.5         0.0  0.0
...         ...  ...
93.5        1.0  1.0
94.5        1.0  0.0
95.5        1.0  1.0
96.5        1.0  0.0
97.5        1.0  1.0
98.5        1.0  0.0
99.5        1.0  1.0
dtype: float64, shape: (100, 2)

decoded is now a TsdFrame object containing the decoded features for each time bin.

>>> dist
Time (s)
----------  --------------------------
0.5         [[0.      , 1.333333] ...]
1.5         [[1.333333, 0.      ] ...]
2.5         [[0.      , 1.333333] ...]
3.5         [[1.333333, 0.      ] ...]
4.5         [[0.      , 1.333333] ...]
5.5         [[1.333333, 0.      ] ...]
...
94.5        [[1.333333, 1.333333] ...]
95.5        [[1.333333, 1.333333] ...]
96.5        [[1.333333, 1.333333] ...]
97.5        [[1.333333, 1.333333] ...]
98.5        [[1.333333, 1.333333] ...]
99.5        [[1.333333, 1.333333] ...]
dtype: float64, shape: (100, 2, 2)

and dist is a TsdTensor object containing the distances for each time bin.

It is also possible to pass continuous values instead of spikes (e.g. calcium imaging):

>>> time = np.arange(0,100, 0.1)
>>> group = nap.TsdFrame(t=time, d=np.stack([time % 0.5, time %1], axis=1))
>>> tuning_curves = nap.compute_tuning_curves(group, features, bins=2, range=[(-.5, 1.5)]*2)
>>> decoded, dist = nap.decode_template(tuning_curves, group, epochs=epochs, bin_size=1)
>>> decoded
Time (s)    0    1
----------  ---  ---
0.0         0.0  0.0
0.1         0.0  0.0
0.2         0.0  0.0
0.3         0.0  0.0
0.4         0.0  0.0
0.5         1.0  1.0
0.6         1.0  1.0
...         ...  ...
99.3        0.0  0.0
99.4        0.0  0.0
99.5        1.0  1.0
99.6        1.0  1.0
99.7        1.0  1.0
99.8        1.0  1.0
99.9        1.0  1.0
dtype: float64, shape: (1000, 2)