"""
Functions to shuffle timestamps to create surrogate datasets.
"""
import warnings
import numpy as np
from .. import core as nap
[docs]
def shift_timestamps(data, min_shift=0.0, max_shift=None, mode="drop"):
"""
Shifts all the time stamps of a random amount between a minimum and maximum shift, wrapping the
end of the time support to the beginning.
Notes
-----
If the time support of the input has multiple epochs, some timepoints will fall outside of
those epochs after shifting. In ``mode='drop'``, those timepoints are dropped. In
``mode='wrap'``, timestamps are wrapped circularly using the full time support.
However, if there are multiple epochs and ``mode='wrap'``,
timepoints falling outside the epochs in the middle are still dropped.
Parameters
----------
data : Ts, TsGroup
The timeseries object whose timestamps to shift.
If TsGroup, shifts all objects in the group independently.
min_shift : float, optional
minimum shift (default: 0)
max_shift : float, optional
maximum shift, (default: length of time support)
mode : ``'drop'`` or ``'wrap'``, optional
How to handle timestamps that fall outside the time support after shifting.
* ``'drop'``: (default): drop those timestamps
* ``'wrap'``: circularly wrap timestamps within the time support
Returns
-------
Ts or TsGroup
The randomly shifted timestamps
Examples
--------
Fixed shift with default ``mode='drop'``:
>>> import pynapple as nap
>>> ts = nap.Ts([25, 27, 33.3, 34.5])
>>> shifted_ts = nap.shift_timestamps(ts, min_shift=1, max_shift=1, mode="drop")
>>> shifted_ts
Time (s)
26.0
28.0
34.3
shape: 4
The last timepoint falls outside the time support, so it is dropped.
With multiple epochs, timestamps falling outside the support anywhere are dropped:
>>> epochs = nap.IntervalSet(start=[25, 30], end=[27, 34.5])
>>> ts = nap.Ts([25, 27, 33.3, 34.5], time_support=epochs)
>>> shifted_ts = nap.shift_timestamps(ts, min_shift=1, max_shift=1, mode="drop")
>>> shifted_ts
Time (s)
26.0
34.3
shape: 3
Using ``mode='wrap'`` to circularly wrap timestamps within the full time support:
>>> epochs = nap.IntervalSet(start=0, end=40)
>>> ts = nap.Ts([38, 39.5], time_support=epochs)
>>> shifted_ts = nap.shift_timestamps(ts, min_shift=5, max_shift=5, mode="wrap")
>>> shifted_ts
Time (s)
3.0
4.5
shape: 2
When ``mode='wrap'`` and there are multiple epochs, the start and end are circular, but everything in between is not:
>>> import pynapple as nap
>>> epochs = nap.IntervalSet(start=[25, 30], end=[27, 34.5])
>>> ts = nap.Ts([25, 27, 33.3, 34.5], time_support=epochs)
>>> shifted_ts = nap.shift_timestamps(ts, min_shift=1, max_shift=1, mode="wrap")
>>> shifted_ts
Time (s)
26.0
26.0
34.3
shape: 3
"""
if not isinstance(min_shift, (int, float)):
raise TypeError("min_shift should be a number.")
if max_shift is not None and not isinstance(max_shift, (int, float)):
raise TypeError("max_shift should be a number.")
if mode not in ("drop", "wrap"):
raise ValueError("mode must be either 'drop' or 'wrap'.")
if not isinstance(data, (nap.Ts, nap.TsGroup)):
raise TypeError("Invalid input, data should be a Ts or TsGroup.")
time_support = data.time_support
if max_shift is None:
max_shift = time_support.tot_length()
def _shift(data):
shift = np.random.uniform(min_shift, max_shift)
times = data.times()
shifted = times + shift
if mode == "wrap":
start = time_support.start[0]
end = time_support.end[-1]
period = end - start
shifted = start + ((shifted - start) % period)
shifted = np.sort(shifted)
return nap.Ts(t=shifted, time_support=time_support)
if isinstance(data, nap.TsGroup):
shifted = {}
for k in data:
if not isinstance(data[k], nap.Ts):
warnings.warn(
f"TsGroup entry {k} was not a Ts, but treating it as one!",
UserWarning,
)
shifted[k] = _shift(data[k])
return nap.TsGroup(shifted, time_support=time_support)
else:
return _shift(data)
# Random shuffle intervals between timestamps
[docs]
def shuffle_ts_intervals(ts):
"""
Randomizes the timestamps by shuffling the intervals between them.
Parameters
----------
ts : Ts or TsGroup
The timestamps to randomize. If TsGroup, randomizes all Ts in the group independently.
Returns
-------
Ts or TsGroup
The randomized timestamps, with shuffled intervals
"""
strategies = {
nap.time_series.Ts: _shuffle_intervals_ts,
nap.ts_group.TsGroup: _shuffle_intervals_tsgroup,
}
# checks input type
if type(ts) not in strategies.keys():
raise TypeError("Invalid input type, should be Ts or TsGroup")
strategy = strategies[type(ts)]
return strategy(ts)
# Random Jitter
[docs]
def jitter_timestamps(ts, max_jitter=None, keep_tsupport=False):
"""
Jitters each time stamp independently of random amounts uniformly drawn between -max_jitter and max_jitter.
Parameters
----------
ts : Ts or TsGroup
The timestamps to jitter. If TsGroup, jitter is applied to each element of the group.
max_jitter : float
maximum jitter
keep_tsupport: bool, optional
If True, keep time support of the input. The number of timestamps will not be conserved.
If False, the time support is inferred from the jittered timestamps. The number of tmestamps
is conserved. (default: False)
Returns
-------
Ts or TsGroup
The jittered timestamps
"""
strategies = {
nap.time_series.Ts: _jitter_ts,
nap.ts_group.TsGroup: _jitter_tsgroup,
}
# checks input type
if type(ts) not in strategies.keys():
raise TypeError("Invalid input type, should be Ts or TsGroup")
if max_jitter is None:
raise TypeError("missing required argument: max_jitter ")
strategy = strategies[type(ts)]
return strategy(ts, max_jitter, keep_tsupport)
# Random resample
[docs]
def resample_timestamps(ts):
"""
Resamples the timestamps in the time support, with uniform distribution.
Parameters
----------
ts : Ts or TsGroup
The timestamps to resample. If TsGroup, each Ts object in the group is independently
resampled, in the time support of the whole group.
Returns
-------
Ts or TsGroup
The resampled timestamps
"""
strategies = {
nap.time_series.Ts: _resample_ts,
nap.ts_group.TsGroup: _resample_tsgroup,
}
# checks input type
if type(ts) not in strategies.keys():
raise TypeError("Invalid input type, should be Ts or TsGroup")
strategy = strategies[type(ts)]
return strategy(ts)
# Helper functions
def _jitter_ts(ts, max_jitter=None, keep_tsupport=False):
"""
Parameters
----------
ts : Ts
The timestamps to jitter.
max_jitter : float
maximum jitter
keep_tsupport: bool, optional
If True, keep time support of the input. The number of timestamps will not be conserved.
If False, the time support is inferred from the jittered timestamps. The number of tmestamps
is conserved. (default: False)
Returns
-------
Ts
The jittered timestamps
"""
jittered_timestamps = ts.times() + np.random.uniform(
-max_jitter, max_jitter, len(ts)
)
if keep_tsupport:
jittered_ts = nap.Ts(
t=np.sort(jittered_timestamps), time_support=ts.time_support
)
else:
jittered_ts = nap.Ts(t=np.sort(jittered_timestamps))
return jittered_ts
def _jitter_tsgroup(tsgroup, max_jitter=None, keep_tsupport=False):
"""
Jitters each time stamp independently, for each element in the TsGroup
of random amounts uniformly drawn between -max_jitter and max_jitter.
Parameters
----------
tsgroup : TsGroup
The timestamps to jitter, the jitter is applied to each element of the group.
max_jitter : float
maximum jitter
keep_tsupport: bool, optional
If True, keep time support of the input. The number of timestamps will not be conserved.
If False, the time support is inferred from the jittered timestamps. The number of tmestamps
is conserved. (default: False)
Returns
-------
TsGroup
The jittered timestamps
"""
jittered_tsgroup = {}
for k in tsgroup.keys():
jittered_timestamps = tsgroup[k].times() + np.random.uniform(
-max_jitter, max_jitter, len(tsgroup[k])
)
jittered_tsgroup[k] = nap.Ts(t=np.sort(jittered_timestamps))
if keep_tsupport:
jittered_tsgroup = nap.TsGroup(
jittered_tsgroup, time_support=tsgroup.time_support
)
else:
jittered_tsgroup = nap.TsGroup(jittered_tsgroup)
return jittered_tsgroup
def _resample_ts(ts):
"""
Resamples the timestamps in the time support, with uniform distribution.
Parameters
----------
ts : Ts
The timestamps to resample.
Returns
-------
Ts
The resampled timestamps
"""
resampled_timestamps = np.random.uniform(ts.start_time(), ts.end_time(), len(ts))
resampled_ts = nap.Ts(t=np.sort(resampled_timestamps), time_support=ts.time_support)
return resampled_ts
def _resample_tsgroup(tsgroup):
"""
Resamples the each timestamp series in the group, with uniform distribution and on the time
support of the whole group.
Parameters
----------
tsgroup : TsGroup
The TsGroup to resample, each Ts object in the group is independently
resampled, in the time support of the whole group.
Returns
-------
TsGroup
The resampled TsGroup
"""
start_time = tsgroup.time_support.start[0]
end_time = tsgroup.time_support.end[0]
resampled_tsgroup = {}
for k in tsgroup.keys():
resampled_timestamps = np.random.uniform(start_time, end_time, len(tsgroup[k]))
resampled_tsgroup[k] = nap.Ts(t=np.sort(resampled_timestamps))
return nap.TsGroup(resampled_tsgroup, time_support=tsgroup.time_support)
def _shuffle_intervals_ts(ts):
"""
Randomizes the timestamps by shuffling the intervals between them.
Parameters
----------
ts : Ts
The timestamps to randomize.
Returns
-------
Ts
The timestamps with shuffled intervals
"""
intervals = np.diff(ts.times())
shuffled_intervals = np.random.permutation(intervals)
start_time = ts.times()[0]
randomized_timestamps = np.hstack(
[start_time, start_time + np.cumsum(shuffled_intervals)]
)
randomized_ts = nap.Ts(t=randomized_timestamps)
return randomized_ts
def _shuffle_intervals_tsgroup(tsgroup):
"""
Randomizes the timestamps by shuffling the intervals between them.
Each Ts in the group is randomized independently
Parameters
----------
tsgroup : TsGroup
The TsGroup to randomize.
Returns
-------
tsGroup
The TsGroup with shuffled intervals.
"""
randomized_tsgroup = {k: _shuffle_intervals_ts(tsgroup[k]) for k in tsgroup.keys()}
return nap.TsGroup(randomized_tsgroup)