Basic Usage

import numpy as np

from rhythmic_segments import RhythmicSegments

Rhythmic Segment Analysis

A rhythmic segment analysis (RSA) analyzes every fixed-length segment of a sequence of time intervals: the short groups you obtain by sliding a window across the data. Each segment has a duration and a pattern. The pattern captures the relative durations of a segment’s intervals, either as a normalized vector or as a ratio. For example, the segment \((2, 4, 4)\) has the pattern \((0.2, 0.4, 0.4)\) or \(1 : 2 : 2\); both descriptions are interchangeable. Thinking of patterns as normalized vectors shows that all patterns of a given length live on a simplex: a line when \(n = 2\), a triangle when \(n = 3\), and so on. The goal is to study rhythmic material by analyzing how its segments are distributed on that simplex.

Computing patterns is as simple as normalizing the segment:

segment = np.array([2, 4, 4])
pattern = segment / segment.sum()
pattern
array([0.2, 0.4, 0.4])

And so you can absolutely do a rhythmic segment analysis without using this package. This package however provides some utilities that make things easier. In particular, the RhythmicSegments class allows you to conveniently store large numbers of segments and handle associated metadata, and makes them accessible via .segments, .patterns, .durations, .meta.

Segments

You can create a RhythmicSegments store from segment data, from interval data, or from event data such as note onset times. These require successively more preprocessing: from_events will compute the intervals, and then call from_intervals, which will extract all segments and then calls from_segments. The simplest, but one you won’t use in practice is therefore from_segments:

segments = [[2, 8], [.3, .6], [1, 1]]
rs = RhythmicSegments.from_segments(segments)
rs.patterns
array([[0.2       , 0.8       ],
       [0.33333334, 0.6666667 ],
       [0.5       , 0.5       ]], dtype=float32)

Intervals

In practice, you will usually start from either events or intervals, and use the class to extract the segments for you:

intervals = [1, 2, 3, 4, 5, 6, 7, 8, 9]
rs = RhythmicSegments.from_intervals(intervals, length=3)
rs.segments
array([[1., 2., 3.],
       [2., 3., 4.],
       [3., 4., 5.],
       [4., 5., 6.],
       [5., 6., 7.],
       [6., 7., 8.],
       [7., 8., 9.]], dtype=float32)

Often, interval data is composed of multiple blocks (e.g. bouts, songs, etc.) and segments should not cross block boundaries. RhythmicSegments will treat np.nan entries as block boundaries, unless split_at_nan=False.

intervals = [1, 2, 3, 4, np.nan, 5, 6, np.nan, 7, 8, 9, np.nan]
rs = RhythmicSegments.from_intervals(intervals, length=2)
rs.segments 
array([[1., 2.],
       [2., 3.],
       [3., 4.],
       [5., 6.],
       [7., 8.],
       [8., 9.]], dtype=float32)

Event data

Finally, you can also start with event data such as onsets. Again, NaN entries are treated as boundaries:

onsets = [0, 2, 3, np.nan, 4, 6, 9, 10]
rs = RhythmicSegments.from_events(onsets, length=2)
rs.segments
array([[2., 1.],
       [2., 3.],
       [3., 1.]], dtype=float32)

Dataframes

You can also pass a pandas DataFrame to create a RhythmicSegments object. You do have to specify which column contains the event/interval data:

import pandas as pd

df = pd.DataFrame(dict(
  onsets = [0, 2, 3, np.nan, 4, 6, 9, 10], 
  labels=["a", "b", "c", np.nan, "d", "e", "f", "g"]
))

rs = RhythmicSegments.from_events(df, column='onsets', length=2)
rs.segments
array([[2., 1.],
       [2., 3.],
       [3., 1.]], dtype=float32)

In this case the other columns will be added as metadata (more about that in another notebook):

rs.meta
labels_1 labels_2
0 a b
1 d e
2 e f