Tuning curves#
Pynapple can compute 1-dimensional tuning curves (for example firing rate as a function of angular direction) and 2-dimensional tuning curves (for example firing rate as a function of position). It can also compute average firing rate for different epochs (for example firing rate for different epochs of stimulus presentation).
Important
If you are using calcium imaging data with the activity of the cell as a continuous transient, the function to call ends with _continuous
for continuous time series (e.g. compute_1d_tuning_curves_continuous
).
Show code cell content
import pynapple as nap
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
custom_params = {"axes.spines.right": False, "axes.spines.top": False}
sns.set_theme(style="ticks", palette="colorblind", font_scale=1.5, rc=custom_params)
Show code cell content
group = {
0: nap.Ts(t=np.sort(np.random.uniform(0, 100, 10))),
1: nap.Ts(t=np.sort(np.random.uniform(0, 100, 20))),
2: nap.Ts(t=np.sort(np.random.uniform(0, 100, 30))),
}
tsgroup = nap.TsGroup(group)
From epochs#
The epochs should be stored in a dictionnary:
dict_ep = {
"stim0": nap.IntervalSet(start=0, end=20),
"stim1":nap.IntervalSet(start=30, end=70)
}
nap.compute_discrete_tuning_curves
takes a TsGroup
for spiking activity and a dictionary of epochs.
The output is a pandas DataFrame where each column is a unit in the TsGroup
and each row is one IntervalSet
type.
The value is the mean firing rate of the neuron during this set of intervals.
mean_fr = nap.compute_discrete_tuning_curves(tsgroup, dict_ep)
pprint(mean_fr)
0 1 2
stim0 0.100 0.200 0.200
stim1 0.075 0.125 0.325
From timestamps activity#
1-dimension tuning curves#
Show code cell content
from scipy.ndimage import gaussian_filter1d
# Fake Tuning curves
N = 6 # Number of neurons
bins = np.linspace(0, 2*np.pi, 61)
x = np.linspace(-np.pi, np.pi, len(bins)-1)
tmp = np.roll(np.exp(-(1.5*x)**2), (len(bins)-1)//2)
generative_tc = np.array([np.roll(tmp, i*(len(bins)-1)//N) for i in range(N)]).T
# Feature
T = 50000
dt = 0.002
timestep = np.arange(0, T)*dt
feature = nap.Tsd(
t=timestep,
d=gaussian_filter1d(np.cumsum(np.random.randn(T)*0.5), 20)%(2*np.pi)
)
index = np.digitize(feature, bins)-1
# Spiking activity
count = np.random.poisson(generative_tc[index])>0
tsgroup = nap.TsGroup(
{i:nap.Ts(timestep[count[:,i]]) for i in range(N)},
time_support = nap.IntervalSet(0, 100)
)
Mandatory arguments are TsGroup
, Tsd
(or TsdFrame
with 1 column only)
and nb_bins
for number of bins of the tuning curves.
If an IntervalSet
is passed with ep
, everything is restricted to ep
otherwise the time support of the feature is used.
The min and max of the tuning curve is by default the min and max of the feature. This can be tweaked with the argument minmax
.
The output is a pandas DataFrame. Each column is a unit from TsGroup
argument. The index of the DataFrame carries the center of the bin in feature space.
tuning_curve = nap.compute_1d_tuning_curves(
group=tsgroup,
feature=feature,
nb_bins=120,
minmax=(0, 2*np.pi)
)
print(tuning_curve)
0 1 2 3 4 5
0.026180 308.241459 63.530682 0.000000 0.0 0.0 29.412353
0.078540 308.761936 49.540161 0.000000 0.0 0.0 27.650323
0.130900 300.449468 59.868160 0.000000 0.0 0.0 14.412705
0.183260 287.691529 66.880318 1.061592 0.0 0.0 21.231847
0.235619 260.598432 114.409068 1.059343 0.0 0.0 8.474746
... ... ... ... ... ... ...
6.047566 302.304897 8.046138 0.000000 0.0 0.0 96.553655
6.099926 319.044038 20.920921 0.000000 0.0 0.0 78.453452
6.152286 316.182794 23.109706 0.000000 0.0 0.0 59.875147
6.204645 311.170119 34.442494 0.000000 0.0 0.0 52.257577
6.257005 299.049043 27.512512 0.000000 0.0 0.0 51.436435
[120 rows x 6 columns]
plt.figure()
plt.plot(tuning_curve)
plt.xlabel("Feature space")
plt.ylabel("Firing rate (Hz)")
plt.show()

Internally, the function is calling the method value_from
which maps timestamps to their closest values in time from a Tsd
object. It is then possible to validate the tuning curves by displaying the timestamps as well as their associated values.
Show code cell source
plt.figure()
plt.subplot(121)
plt.plot(tsgroup[3].value_from(feature), 'o')
plt.plot(feature, label="feature")
plt.ylabel("Feature")
plt.xlim(0, 2)
plt.xlabel("Time (s)")
plt.subplot(122)
plt.plot(tuning_curve[3].values, tuning_curve[3].index.values, label="Tuning curve (unit=3)")
plt.xlabel("Firing rate (Hz)")
plt.legend()
plt.show()

2-dimensional tuning curves#
Show code cell content
dt = 0.01
T = 10
epoch = nap.IntervalSet(start=0, end=T, time_units="s")
features = np.vstack((np.cos(np.arange(0, T, dt)), np.sin(np.arange(0, T, dt)))).T
features = nap.TsdFrame(
t=np.arange(0, T, dt),
d=features,
time_units="s",
time_support=epoch,
columns=["a", "b"],
)
tsgroup = nap.TsGroup({
0: nap.Ts(t=np.sort(np.random.uniform(0, T, 10))),
1: nap.Ts(t=np.sort(np.random.uniform(0, T, 15))),
2: nap.Ts(t=np.sort(np.random.uniform(0, T, 20))),
}, time_support=epoch)
The group
argument must be a TsGroup
object.
The features
argument must be a 2-columns TsdFrame
object.
nb_bins
can be an int or a tuple of 2 ints.
tcurves2d, binsxy = nap.compute_2d_tuning_curves(
group=tsgroup,
features=features,
nb_bins=(5,5),
minmax=(-1, 1, -1, 1)
)
pprint(tcurves2d)
{0: array([[0. , 1.2345679 , 1.25 , 0. , 0. ],
[2.22222222, nan, nan, nan, 1.13636364],
[2.5 , nan, nan, nan, 0. ],
[0. , nan, nan, nan, 0. ],
[0. , 2.22222222, 3.27868852, 2.27272727, 0. ]]),
1: array([[3.57142857, 0. , 2.5 , 1.12359551, 1.75438596],
[0. , nan, nan, nan, 0. ],
[2.5 , nan, nan, nan, 1.2345679 ],
[2.27272727, nan, nan, nan, 0. ],
[3.57142857, 2.22222222, 0. , 3.40909091, 3.50877193]]),
2: array([[0. , 1.2345679 , 5. , 3.37078652, 3.50877193],
[4.44444444, nan, nan, nan, 0. ],
[2.5 , nan, nan, nan, 1.2345679 ],
[2.27272727, nan, nan, nan, 0. ],
[3.57142857, 2.22222222, 1.63934426, 1.13636364, 1.75438596]])}
/home/runner/.local/lib/python3.12/site-packages/pynapple/process/tuning_curves.py:269: RuntimeWarning: invalid value encountered in divide
count = count / occupancy
tcurves2d
is a dictionnary with each key a unit in TsGroup
. binsxy
is a numpy array representing the centers of the bins and is useful for plotting tuning curves. Bins that have never been visited by the feature have been assigned a NaN value.
Checking the accuracy of the tuning curves can be bone by displaying the spikes aligned to the features with the function value_from
which assign to each spikes the corresponding features value for unit 0.
ts_to_features = tsgroup[0].value_from(features)
print(ts_to_features)
Time (s) a b
---------- -------- --------
0.130159 0.99156 0.12963
0.194815 0.982 0.18886
0.521236 0.86782 0.49688
0.548185 0.85252 0.52269
2.9484 -0.9817 0.19042
3.63476 -0.88308 -0.46922
4.22784 -0.4639 -0.88589
4.68345 -0.03238 -0.99948
5.86956 0.91585 -0.40153
8.274 -0.40412 0.91471
dtype: float64, shape: (10, 2)
tsgroup[0]
which is a Ts
object has been transformed to a TsdFrame
object with each timestamps (spike times) being associated with a features value.
Show code cell source
plt.figure()
plt.subplot(121)
plt.plot(features["b"], features["a"], label="features")
plt.plot(ts_to_features["b"], ts_to_features["a"], "o", color="red", markersize=4, label="spikes")
plt.xlabel("feature b")
plt.ylabel("feature a")
[plt.axvline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
[plt.axhline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
plt.subplot(122)
extents = (
np.min(features["a"]),
np.max(features["a"]),
np.min(features["b"]),
np.max(features["b"]),
)
plt.imshow(tcurves2d[0],
origin="lower", extent=extents, cmap="viridis",
aspect='auto'
)
plt.title("Tuning curve unit 0")
plt.xlabel("feature b")
plt.ylabel("feature a")
plt.grid(False)
plt.colorbar()
plt.tight_layout()
plt.show()

From continuous activity#
Tuning curves compute with the following functions are usually made with data from calcium imaging activities.
1-dimensional tuning curves#
Show code cell content
from scipy.ndimage import gaussian_filter1d
# Fake Tuning curves
N = 3 # Number of neurons
bins = np.linspace(0, 2*np.pi, 61)
x = np.linspace(-np.pi, np.pi, len(bins)-1)
tmp = np.roll(np.exp(-(1.5*x)**2), (len(bins)-1)//2)
generative_tc = np.array([np.roll(tmp, i*(len(bins)-1)//N) for i in range(N)]).T
# Feature
T = 50000
dt = 0.002
timestep = np.arange(0, T)*dt
feature = nap.Tsd(
t=timestep,
d=gaussian_filter1d(np.cumsum(np.random.randn(T)*0.5), 20)%(2*np.pi)
)
index = np.digitize(feature, bins)-1
tmp = generative_tc[index]
tmp = tmp + np.random.randn(*tmp.shape)*1
# Calcium activity
tsdframe = nap.TsdFrame(
t=timestep,
d=tmp
)
Arguments are TsdFrame
(for example continuous calcium data), Tsd
or TsdFrame
for the 1-d feature and nb_bins
for the number of bins.
tuning_curves = nap.compute_1d_tuning_curves_continuous(
tsdframe=tsdframe,
feature=feature,
nb_bins=120,
minmax=(0, 2*np.pi)
)
print(tuning_curves)
0 1 2
0.026180 1.044605 -0.013736 -0.020407
0.078540 0.936195 0.048169 0.033389
0.130900 0.987900 -0.013681 0.018505
0.183260 0.992176 0.042529 0.016079
0.235619 0.833251 -0.028489 0.019821
... ... ... ...
6.047566 0.887646 0.008884 -0.009700
6.099926 1.009504 0.025007 -0.047017
6.152286 0.996669 -0.018629 -0.047925
6.204645 0.983834 0.021691 -0.008538
6.257005 0.996730 -0.080395 0.026000
[120 rows x 3 columns]
plt.figure()
plt.plot(tuning_curves)
plt.xlabel("Feature space")
plt.ylabel("Mean activity")
plt.show()

2-dimensional tuning curves#
Show code cell content
dt = 0.01
T = 10
epoch = nap.IntervalSet(start=0, end=T, time_units="s")
features = np.vstack((np.cos(np.arange(0, T, dt)), np.sin(np.arange(0, T, dt)))).T
features = nap.TsdFrame(
t=np.arange(0, T, dt),
d=features,
time_units="s",
time_support=epoch,
columns=["a", "b"],
)
# Calcium activity
tsdframe = nap.TsdFrame(
t=timestep,
d=np.random.randn(len(timestep), 2)
)
Arguments are TsdFrame
(for example continuous calcium data), Tsd
or TsdFrame
for the 1-d feature and nb_bins
for the number of bins.
tuning_curves, xy = nap.compute_2d_tuning_curves_continuous(
tsdframe=tsdframe,
features=features,
nb_bins=5,
)
print(tuning_curves)
{0: array([[-0.0392745 , 0.03301733, -0.02430503, -0.07893938, 0.11182735],
[ 0.04592458, nan, nan, nan, 0.06572145],
[ 0.00191837, nan, nan, nan, -0.05937694],
[ 0.02461932, nan, nan, nan, -0.02639447],
[ 0.10284367, 0.00581385, 0.10111825, -0.00861396, 0.01268855]]), 1: array([[-0.0720635 , 0.09278164, 0.03040518, -0.03720248, -0.05505701],
[-0.10300814, nan, nan, nan, 0.03561087],
[-0.07976564, nan, nan, nan, 0.00385543],
[ 0.03268543, nan, nan, nan, -0.00861405],
[-0.03922301, -0.00016053, -0.0385757 , 0.02269574, -0.13731532]])}
/home/runner/.local/lib/python3.12/site-packages/numpy/_core/fromnumeric.py:3860: RuntimeWarning: Mean of empty slice.
return _methods._mean(a, axis=axis, dtype=dtype,
/home/runner/.local/lib/python3.12/site-packages/numpy/_core/_methods.py:137: RuntimeWarning: invalid value encountered in divide
ret = um.true_divide(
plt.figure()
plt.subplot(121)
plt.plot(features["b"], features["a"], label="features")
plt.xlabel("feature b")
plt.ylabel("feature a")
[plt.axvline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
[plt.axhline(b, linewidth=0.5, color='grey') for b in np.linspace(-1, 1, 6)]
plt.subplot(122)
extents = (
np.min(features["a"]),
np.max(features["a"]),
np.min(features["b"]),
np.max(features["b"]),
)
plt.imshow(tuning_curves[0],
origin="lower", extent=extents, cmap="viridis",
aspect='auto'
)
plt.title("Tuning curve unit 0")
plt.xlabel("feature b")
plt.ylabel("feature a")
plt.grid(False)
plt.colorbar()
plt.tight_layout()
plt.show()
