Skip to content

A find_peaks method for Tsd and TsdFrame#591

Merged
gviejo merged 4 commits intodevfrom
550-find-peaks
Apr 20, 2026
Merged

A find_peaks method for Tsd and TsdFrame#591
gviejo merged 4 commits intodevfrom
550-find-peaks

Conversation

@wulfdewolf
Copy link
Copy Markdown
Collaborator

This PR adds a find_peaks method to Tsd and TsdFrame.
The method mostly wraps scipy.signal.find_peaks, but ensures correct epochs and Pynapple formatting.

Usage

For Tsd:

        >>> import pynapple as nap
        >>> import numpy as np
        >>> times = np.arange(0, 10, 0.1)
        >>> tsd = nap.Tsd(t=times, d=np.sin(times))
        >>> peaks = tsd.find_peaks()
        >>> peaks
        Time (s)
        ----------  --------
        1.6         0.999574
        7.9         0.998941
        dtype: float64, shape: (2,)

You can set various requirements for finding peaks, for example a minimum width (these are just passed to scipy.signal.find_peaks):

        >>> peaks = tsd.find_peaks(width=21)
        >>> peaks
        Time (s)
        ----------  --------
        7.9         0.998941
        dtype: float64, shape: (1,)

If you further want the peak properties returned, you can pass return_prop=True:

        >>> peaks = tsd.find_peaks(return_prop=True, width=21)
        >>> peaks
        Time (s)      peak_value    prominences    left_bases    right_bases    widths  ...
        ----------  ------------  -------------  ------------  -------------  --------  -----
        7.9             0.998941        1.45648            47             99   25.9266  ...
        dtype: float64, shape: (1, 8)

For TsdFrame:

It works similarly, but we return a TsGroup containing the same outputs per column:

        >>> import pynapple as nap
        >>> import numpy as np
        >>> times = np.arange(0, 10, 0.1)
        >>> tsdframe = nap.TsdFrame(t=times, d=np.stack([np.sin(times), np.cos(times)], axis=1))
        >>> peaks = tsdframe.find_peaks()
        >>> peaks
          Index     rate
        -------  -------
              0  0.20202
              1  0.10101
        >>> peaks[0]
        Time (s)
        ----------  --------
        1.6         0.999574
        7.9         0.998941
        dtype: float64, shape: (2,)

You can again set various requirements for finding peaks, for example a minimum width:

        >>> peaks = tsdframe.find_peaks(width=21)
        >>> peaks
          Index     rate
        -------  -------
              0  0.10101
              1  0.10101
        >>> peaks[0]
        Time (s)
        ----------  --------
        7.9         0.998941
        dtype: float64, shape: (1,)

If you want the peak properties returned, you can again pass return_prop=True:

        >>> peaks = tsdframe.find_peaks(return_prop=True, width=21)
        >>> peaks
          Index     rate
        -------  -------
              0  0.10101
              1  0.10101
        >>> peaks[0]
        Time (s)      peak_value    prominences    left_bases    right_bases    widths  ...
        ----------  ------------  -------------  ------------  -------------  --------  -----
        7.9             0.998941        1.45648            47             99   25.9266  ...
        dtype: float64, shape: (1, 8)

Documentation

Both methods have docstrings, including examples.
In a future PR we will use these methods in the tutorials, but for the moment I don't think they need a user guide.

Testing

Adds thorough testing for the find_peaks methods for both classes, checking input/output types, correct peak extraction, metadata, etc.

Related issues

#550 #584

Questions for reviewers

In the case of a TsdFrame we return a TsGroup containing entries per column.
However, TsGroup requires integer keys, whereas TsdFrame columns can be strings.
The current implementation sets the TsGroup keys to be indices starting from 0.
That might not be very intuitive for the user if they do have string columns.

A simplification would be to return a dict instead of a TsGroup, but then we won't be tracking the time_support.

@wulfdewolf wulfdewolf requested a review from gviejo as a code owner April 17, 2026 10:17
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
pynapple/core/time_series.py 93.43% <100.00%> (+0.20%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gviejo gviejo merged commit 62c436d into dev Apr 20, 2026
18 of 31 checks passed
@gviejo gviejo deleted the 550-find-peaks branch April 20, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants