pyriodic package#

circular#

class pyriodic.circular.Circular(data: ndarray, labels: list | ndarray | None = None, unit: str = 'radians', full_range: tuple | None = None)#

Bases: object

A class for representing and working with circular data (e.g., angles, time-of-day, phase).

This class supports circular statistics and visualisation of data that wraps around a fixed range, such as angles (in radians or degrees), phases, hours, or other cyclic measurements. It provides basic unit validation, conversion between degrees and radians, and visualisation tools.

Parameters:
  • data (array-like) – A sequence of numerical values representing circular measurements. Values should be in the range appropriate to the specified unit.

  • labels (array-like of str or int, optional) – Optional condition labels or identifiers corresponding to each data point.

  • unit (str, optional) – The unit of the input data. Must be one of {“radians”, “degrees”}. Default is “radians”. Assumes radian range to be from 0 to 2pi.

  • full_range (tuple, optional)

data#

The circular data as a NumPy array.

Type:

ndarray

labels#

Optional labels (e.g., condition tags) for each data point.

Type:

ndarray or list

unit#

Unit of measurement, either “radians” or “degrees”.

Type:

str

classmethod from_multiple(circular_objects, labels=None)#
mean(group_by_label: bool = False)#

Compute the circular mean of the data.

Parameters:

group_by_label (bool, default=False) – If True, computes the mean for each label group separately.

Returns:

mean – The circular mean angle, either as a single value or a dictionary of means by label.

Return type:

float or dict

median(group_by_label: bool = False)#

Compute the circular median of the data.

Parameters:

group_by_label (bool, default=False) – If True, computes the median for each label group separately.

Returns:

median – The circular median angle, either as a single value or a dictionary of medians by label.

Return type:

float or dict

plot(ax=None, histogram=None, group_by_labels=False)#
r(group_by_label: bool = False)#

Compute mean resultant length of the data, which is a measure of the data’s concentration.

Parameters:

group_by_label (bool, default=False) – If True, computes the median for each label group separately.

Returns:

r – The mean resultant length, either as a single value or a dictionary of values by label.

Return type:

float or dict

density#

pyriodic.density.vonmises_kde(data, kappa, min_x=0, max_x=6.283185307179586, n_bins=100)#

desc#

pyriodic.desc.angular_deviation(rad: ndarray)#
pyriodic.desc.circular_mean(rad: ndarray, wrap_to_2pi: bool = True)#

Compute the mean angle from a list of angles in radians.

Parameters:#

radnp.ndarray

Array of angles in radians.

wrap_to_2pibool, default=True

If True, wraps result to range [0, 2π]. If False, result is in [-π, π].

Returns:#

meanfloat

The circular mean angle.

pyriodic.desc.circular_median(rad: ndarray)#

Compute the median angle from a list of angles in radians.

Parameters:#

radnp.ndarray

Array of angles in radians.

Returns:#

median : float

The circular median angle.

pyriodic.desc.circular_r(rad: ndarray)#

Compute the length of the mean resultant vector $r$, a measure of circular concentration.

\[r = \sqrt{\bar{C}^2 + \bar{S}^2}\]
pyriodic.desc.circular_standard_deviation(rad: ndarray)#

permutation#

pyriodic.permutation.ecdf(data: ndarray, points: ndarray) ndarray#

Compute the Empirical Cumulative Distribution Function (ECDF) of the given data at specified points.

Parameters:
  • data (np.ndarray) – The input sample data.

  • points (np.ndarray) – The points at which to evaluate the ECDF.

  • Returns

  • np.ndarray (-)

pyriodic.permutation.paired_permutation_test(sample1: ndarray, sample2: ndarray, alternative: Literal['greater', 'less', 'two-sided'] = 'greater', n_permutations: int = 1000, rng: Generator | None = None, verbose: bool = True, return_null_distribution: bool = False) tuple#

Performs a permutation test on paired samples (e.g., group-level stat comparison).

pyriodic.permutation.peak_to_peak_modulation(phases: ndarray, values: ndarray, n_bins: int = 8, min_bin_count: int = 10) float#

Computes the peak-to-peak modulation (max - min) of a continuous variable binned by circular phase.

Parameters:
  • phases (np.ndarray) – 1D array of phase values (in radians, from 0 to 2π).

  • values (np.ndarray) – 1D array of continuous values (e.g., reaction times), same length as phases.

  • n_bins (int, default=8) – Number of phase bins to divide the cycle into.

  • min_bin_count (int, default=10) – Minimum number of samples required in each bin. If any bin has fewer, an error is raised.

Returns:

modulation – Difference between the max and min average value across phase bins.

Return type:

float

pyriodic.permutation.permutation_test_against_null(observed: ndarray, phase_pool: ndarray | Literal['uniform'] = 'uniform', n_null: int = 1000, n_permutations: int = 1000, stat_fun: Callable | None = None, alternative: Literal['greater', 'less', 'two-sided'] = 'greater', rng: Generator | None = None, n_bins: int = 30, verbose: bool = True, return_null_samples=False, return_obs_and_null_stats=False) tuple#

Compares an observed circular phase distribution to a null distribution using a Mann–Whitney U-based permutation test on test statistic samples.

pyriodic.permutation.permutation_test_between_samples(sample1: ndarray, sample2: ndarray, stat_fun: Callable | None = None, alternative: Literal['greater', 'less', 'two-sided'] = 'greater', n_permutations: int = 1000, rng: Generator | None = None, n_bins: int = 30, verbose: bool = True, return_null_distribution: bool = False) tuple#

NOTE: Documentation to be added

pyriodic.permutation.permutation_test_phase_modulation(phases: ndarray, values: ndarray, stat_fun: Callable | None = None, n_null: int = 1000, n_bins: int = 8, min_bin_count: int = 10, alternative: Literal['greater', 'less', 'two-sided'] = 'greater', rng: Generator | None = None, verbose: bool = True, return_null_distribution: bool = False) tuple#

Perform a permutation test to assess whether a linear variable is modulated by circular phase.

Parameters:
  • phases (np.ndarray) – 1D array of circular phase values (in radians).

  • values (np.ndarray) – 1D array of scalar values (e.g., RTs) to test for phase-dependent modulation.

  • stat_fun (Callable, optional) – Function that computes a modulation index given (phases, values). Defaults to peak-to-peak modulation across phase bins.

  • n_null (int, default=1000) – Number of permutations to build the null distribution.

  • n_bins (int, default=8) – Number of bins to use when binning phase (passed to stat_fun).

  • min_bin_count (int, default=10) – Minimum number of samples per bin (passed to stat_fun).

  • alternative ({"greater", "less", "two-sided"}, default="greater") – The alternative hypothesis to test.

  • rng (np.random.Generator, optional) – Random number generator for reproducibility.

  • verbose (bool, default=True) – If True, prints the observed statistic and p-value.

  • return_null_distribution (bool, default=False) – If True, also returns the null distribution of test statistics.

Returns:

  • obs_stat (float) – The observed modulation index.

  • p_val (float) – P-value of the observed statistic under the null distribution.

  • null_dist (np.ndarray, optional) – The null distribution (if return_null_distribution is True).

pyriodic.permutation.watson_u2(sample1: ndarray, sample2: ndarray, n_bins: int = 30) float#

Compute the Watson U² statistic between two circular samples.

This is a non-parametric test statistic used to compare two samples of circular data, based on the squared differences between their ECDFs.

Parameters: - sample1 (np.ndarray): First sample of circular data (values should be in [0, 2π]). - sample2 (np.ndarray): Second sample of circular data (values should be in [0, 2π]). - n_bins (int): Number of points to evaluate the ECDFs on the circle.

Returns: - float: The Watson U² statistic.

phase_events#

pyriodic.phase_events.compute_segment_outliers(segments, threshold=3, sampling_rate=None)#

Identify segment durations that deviate > threshold * SD from the mean. Optionally normalize by sampling_rate.

pyriodic.phase_events.create_phase_events(phase_ts: ndarray, events: ndarray, event_labels: ndarray | None = None, unit: str = 'radians', first_samp: int = 0, rejection_method: str | None = None, rejection_criterion: float | None = None, bad_segments: ndarray | None = None, return_rejected: bool = False) Circular | tuple[Circular, list[int]]#

Create Circular object from a phase angle time series and event markers, optionally applying rejection criteria.

Parameters:
  • phase_ts (np.ndarray) – 1D array of phase values (in radians), typically spanning multiple 0–2π cycles.

  • events (np.ndarray) – 1D array of sample indices at which to extract phase values.

  • event_labels (np.ndarray, optional) – Labels for grouping events. If None, returns a single Circular object; otherwise, returns a PhaseEvents container grouped by unique labels.

  • unit (str) – Unit of the input phase time series and the desired unit for output Circular objects. Must be either ‘radians’ or ‘degrees’. Internally, all computations are performed in radians.

  • first_samp (int) – Offset to subtract from each event index to align with the phase time series (useful if phase_ts is a segment of a longer recording).

  • rejection_method (str, optional) – Method to reject events based on surrounding phase dynamics. Supported values: ‘segment_duration_sd’ — excludes events during rising/falling phase segments whose durations deviate more than rejection_criterion standard deviations from the mean.

  • rejection_criterion (float, optional) – Threshold (in standard deviation units) for identifying outlier segments. Only used if rejection_method=’segment_duration_sd’. Default is 3.

  • bad_segments (Optional[np.ndarray]) – For ignoring events that fall within specific segments of the phase time series. Shape should be (n_segments, 2), where each row is a (start, stop) index pair in samples.

  • return_rejected (bool) – If True, also return a list of rejected event indices.

Returns:

  • Circular or PhaseEvents – If event_labels is None, returns a single Circular object. Otherwise, returns a PhaseEvents object containing condition-grouped Circular data.

  • (optional) list[int] – If return_rejected is True, returns a second value: the list of event indices that were rejected.

pyriodic.phase_events.get_outlier_sample_indices(segments, outlier_indices)#

Convert outlier segments to a flat set of sample indices for fast lookup.

pyriodic.phase_events.get_phase_segments(phase_ts: ndarray)#

Identify rising (0→π) and falling (π→2π) phase segments in a wrapped phase signal.

Parameters:

phase_ts (np.ndarray) – 1D array of phase values (in radians), assumed to evolve continuously over time.

Returns:

  • rising_segments (list of (start_idx, stop_idx)) – List of index tuples for rising phase segments (0 to π).

  • falling_segments (list of (start_idx, stop_idx)) – List of index tuples for falling phase segments (π to 2π).

preproc#

class pyriodic.preproc.RawSignal(data, fs, info=None, bad_segments: None | ndarray = None)#

Bases: object

annotate_bad_segments(segments: ndarray, unit: Literal['s', 'sample'])#

Annotates bad segments in the signal. :param segments: Array of segments to annotate. Dimensions should be (n, 2) where n is the number of segments. Each segment is defined by a start and end point. :type segments: np.ndarray :param unit: Unit of the segments, either “s” for seconds or “sample” for samples. :type unit: str

convert_seconds_to_samples(array)#
copy()#
filter_bandpass(low=0.1, high=1.0)#
property history#
interpolate_missing()#
phase_hilbert()#

Extract instantaneous phase angle using the Hilbert Transform.

Parameters:

ts (np.ndarray) – 1D time series

Returns:

phase angles in radians (0 to 2π)

Return type:

np.ndarray

phase_onepoint(peak_finder=None, distance=100, prominence=0.01)#

Extract phase by linearly interpolating from 0 to 2π between detected peaks.

Parameters:
  • peak_finder (callable, optional) – Optional custom function of the form func(ts, **kwargs) -> np.ndarray returning peak indices.

  • distance (int) – Minimum distance between peaks, passed to the peak detection algorithm.

  • prominence (float) – Prominence threshold for peak detection.

Returns:

  • phase (np.ndarray) – The interpolated phase values (ranging from 0 to 2π).

  • peaks (np.ndarray) – Indices of the detected peaks used for phase interpolation.

phase_threepoint(peak_finder: Callable[[...], ndarray] | None = None, distance: int = 100, prominence: float | int = 0.01, percentile: float | int = 50, descent_window: int = 5)#

Extract phase using a three-point method: Peak → descending slope → flat region → ascending slope → next peak.

Parameters:
  • peak_finder (callable, optional) – Optional custom function of the form func(ts, **kwargs) -> np.ndarray returning peak indices.

  • distance (int) – Minimum distance between peaks, used if no custom peak finder is provided.

  • prominence (float) – Prominence threshold for peak detection.

  • percentile (float) – Percentile of the absolute gradient below which a region is considered flat.

  • descent_window (int) – Number of samples used to confirm descending and ascending slopes before and after flat regions.

Returns:

  • phase (np.ndarray) – Phase array in radians (0 to 2π).

  • peaks (np.ndarray) – Indices of detected peaks.

  • troughs (list of tuple) – Each tuple contains the (start_index, end_index) of a flat region between peaks.

phase_twopoint(peak_finder=None, distance=1, prominence=0.01)#

Extract phase using linear interpolation between peaks and troughs.

Parameters:
  • peak_finder (callable, optional) – Optional custom function of the form func(ts, **kwargs) -> np.ndarray returning peak indices.

  • distance (int) – Minimum distance in seconds between peaks, passed to the peak detection algorithm.

  • prominence (float) – Prominence threshold for peak detection.

Returns:

  • phase (np.ndarray) – The extracted phase values (in radians).

  • peaks (np.ndarray) – Indices of detected peaks.

  • troughs (np.ndarray) – Indices of detected troughs.

plot(ax=None, start=0, duration=20)#

Plots the time series data.

Parameters:
  • ax – Matplotlib axis to plot on. If None, creates a new figure.

  • start – Start time in seconds.

  • duration – Duration in seconds to plot.

remove_outliers(threshold=2.5, linear_interpolation=True)#

Removes outliers …. threshold + linear interpolation

resample(sfreq=typing.Union[int, float])#
Parameters:

sfreq – New sampling rate

smoothing(window_size: float = 50.0)#

Smooths the timeseries by calculating the moving average

Parameters:

window_size (float) – Size of the moving window in milliseconds. Default is 50 milliseconds.

zscore()#

Normalises the signal in-place to mean 0 and unit variance.

regression#

utils#

pyriodic.utils.calculate_p_value(observed_stat: float, null_distribution: ndarray, alternative: Literal['greater', 'less', 'two-sided'] = 'greater') float#

Compute the p-value for a given observed statistic against a null distribution.

Parameters:
  • observed_stat (float) – The observed test statistic.

  • null_distribution (np.ndarray) – Array of test statistics computed under the null hypothesis.

  • alternative ({'greater', 'less', 'two-sided'}, default='greater') – Defines the alternative hypothesis: - ‘greater’: test if observed > null - ‘less’: test if observed < null - ‘two-sided’: test if observed is different from null (absolute deviation)

Returns:

p_value – The computed p-value.

Return type:

float

pyriodic.utils.dat2rad(data: ndarray | float | int, full_range: tuple[float, float] = (0, 360))#
pyriodic.utils.rad2dat(rad: ndarray | float | int, full_range: tuple[float, float] = (0, 360))#
pyriodic.utils.rad2deg(rad: ndarray | float | int) ndarray | float | int#

Convert radians to degrees.

viz#

class pyriodic.viz.CircPlot(circ: Circular, title: str | None = None, group_by_labels: bool = True, colours=None, fig_size=(6, 6), dpi=300, ax=None, radius_lim=None, angles: list | None = None, labels: list | None = None)#

Bases: object

add_arrows(angles: ndarray, lengths: ndarray | None = None, labels: ndarray | None = None, **kwargs)#

Plot arrows at specified angles and lengths.

Parameters:
  • angles (np.ndarray) – 1D array of angles (in radians) where arrows should be drawn.

  • lengths (np.ndarray, optional) – Length of each arrow. If None, all arrows will have unit length.

  • labels (np.ndarray, optional) – Optional label array (same length as angles) for grouping and coloring.

  • kwargs (dict) – Additional keyword arguments passed to ax.arrow. Common examples: width, color, alpha.

add_circular_mean(grouped: bool | None = None, **kwargs)#

Plot mean resultant vector(s) as arrows.

Parameters:
  • grouped (bool, optional) – If None, uses self.group_by_labels. If True, plots a vector per label. If False, plots a single mean vector for all data.

  • kwargs (dict) – Additional keyword arguments passed to ax.arrow.

add_density(kappa=20, n_bins=500, grouped: bool | None = None, **kwargs)#

Add a circular density estimate using Von Mises KDE.

Parameters:
  • kappa (float) – Concentration parameter of the Von Mises distribution (higher = sharper).

  • n_bins (int) – Number of angular bins to evaluate the density on.

  • grouped (bool, optional) – If None, uses self.group_by_labels. If True, plots a density curve for each label separately. If False, plots a single joint density curve for all data.

  • kwargs (dict) – Additional keyword arguments passed to ax.plot (e.g., linewidth, linestyle, alpha).

add_histogram(data: ndarray | None = None, label: str | None = None, bins=36, alpha=0.2, color: str = 'grey')#

Plot histogram as radial bars on the polar axis. If data is None, uses circular data in self.circs.

Parameters: - data: np.array - bins: Number of angular bins (default 36). - alpha: Transparency of bars.

add_hline(y=0, **kwargs)#

Add a horizontal line across the polar plot.

Parameters:
  • y (float) – Y-coordinate for the horizontal line (default 0).

  • kwargs (dict) – Additional keyword arguments passed to ax.axhline (e.g., color, linestyle, linewidth).

add_legend(**kwargs)#

Add a legend to the plot.

Parameters:

kwargs (dict) – Additional arguments passed to ax.legend().

add_points(grouped: bool | None = None, y=None, **kwargs)#

Plot circular data points on the polar axis.

Parameters:
  • grouped (bool, optional) – If None, uses the self.group_by_labels setting. If True, plots each label separately. If False, plots all data points together, ignoring labels.

  • y (float, optional) – Y-coordinate for the points. If None, defaults to 0.5 for all points.

  • kwargs (dict) – Additional keyword arguments passed to ax.scatter, such as s, alpha, or marker.

add_polar_line(angles: ndarray, values: ndarray, errors: ndarray | None = None, color: str | None = 'orange', label: str | None = 'Values', alpha: float = 0.2, **kwargs)#

Plot a line with optional shaded error band on the polar axis.

Parameters:
  • angles (np.ndarray) – 1D array of angles (in radians) for the x-axis.

  • values (np.ndarray) – 1D array of values to plot on the radial axis.

  • errors (np.ndarray, optional) – 1D array of error values (e.g., std or SEM) for shading.

  • color (str, optional) – Color for line and fill.

  • label (str, optional) – Label for the line.

  • alpha (float, optional) – Transparency of the error band.

  • kwargs (dict) – Additional keyword arguments for ax.plot.

prepare_ax(radius_lim=None, angles=None, labels=None, title=None)#

Prepare and customize a polar plot axis (self.ax) with standard aesthetic settings.

Parameters:#

radius_limtuple or None

Optional tuple (min, max) for setting radial (r-axis) limits.

angleslist of float or None

Angles (in degrees) where labels should be placed on the theta axis. Default is [0, 90, 180, 270, 360].

labelslist of str or None

Labels corresponding to angles. Default is [“0/2π”, “”, “π”, “”, “”].

save(filename, **kwargs)#

Save the figure to a file

show()#
ticks_to_degrees(n_ticks=8)#

Convert the theta ticks from radians to degrees.

pyriodic.viz.plot_phase_diagnostics(phase_angles: dict[str, ndarray], fs: int | float, data: ndarray | None = None, events: list | ndarray | None = None, event_labels: list | ndarray | None = None, peaks=None, troughs=None, flat_start_stop=None, window_duration: float = 20.0, title=None, start=0)#
Parameters:
  • phase_angles (dict[str, np.ndarray]) – Dictionary of named phase angle signals (e.g., {“Three-point”: …, “Hilbert”: …}). Each should be 1D array of same length as data.

  • fs (float) – Sampling frequency (Hz).

  • data (np.ndarray, optional) – Raw or preprocessed time series signal (same length as phase arrays).

  • events (list[int] or np.ndarray, optional) – Sample indices of events to mark (e.g., stimulus or response times).

  • event_labels (list[str], optional) – Label for each event (same length as events). Used for color grouping.

  • peaks (list[int] or np.ndarray, optional) – Sample indices of identified peaks.

  • troughs (list[int] or np.ndarray, optional) – Sample indices of identified troughs.

  • flat_start_stop (list[tuple[int, int]] or list[int], optional) – Flat segments as start–stop index tuples or flat indices directly.

  • window_duration (float) – Duration (in seconds) of each visible window in interactive mode.

  • start (float) – Start time (in seconds) for the initial view of the plot.

Returns:

  • fig (matplotlib.figure.Figure) – The created figure.

  • axes (list[matplotlib.axes.Axes]) – The axes used in the plot (one per row: signal + phase tracks).