Skip to content

Features

MgFeatures – a named time-series container for motion and audio descriptors.

:class:MgFeatures holds one or more named feature arrays (e.g. quantity of motion, centroid of motion, optical flow statistics, spectral features) together with shared metadata (sampling rate, time axis, source filename). It is the primary data structure for feeding MGT-python analysis results into machine- learning pipelines.

The design follows conventions established by librosa (feature arrays + sample rate) and MNE-Python (named channels + metadata dict).

Examples

import numpy as np from musicalgestures._features import MgFeatures t = np.linspace(0, 10, 100) feat = MgFeatures( ... data={"qom": np.random.rand(100), "com_x": np.random.rand(100)}, ... times=t, ... sr=25.0, ... source="dancer.avi", ... ) feat.shape (2, 100) arr = feat.to_numpy() # shape (2, 100) df = feat.to_dataframe() # pandas DataFrame, columns = feature names

MgFeatures

MgFeatures(data, times=None, sr=1.0, source=None, metadata=None)

Named time-series container for motion and audio descriptors.

Parameters:

Name Type Description Default
data dict[str, ndarray]

A mapping of {feature_name: 1-D numpy array}. All arrays must have the same length (number of time samples).

required
times ndarray | None

1-D array of time stamps in seconds corresponding to each sample. If None, integer sample indices are used.

None
sr float

Sampling rate of the feature time series in Hz (frames per second for video-derived features, Hz for audio-derived features).

1.0
source str | Path | None

Path to the source file that the features were derived from.

None
metadata dict[str, Any] | None

Optional free-form dictionary of additional metadata (parameters used, processing chain description, etc.).

None

Attributes:

Name Type Description
feature_names list[str]

Names of the features stored in this container.

n_features int

Number of named feature channels.

n_samples int

Number of time samples per channel.

shape tuple[int, int]

(n_features, n_samples)

Source code in musicalgestures/_features.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self,
    data: dict[str, np.ndarray],
    times: np.ndarray | None = None,
    sr: float = 1.0,
    source: str | Path | None = None,
    metadata: dict[str, Any] | None = None,
) -> None:
    if not data:
        raise ValueError("'data' must contain at least one feature array.")
    lengths = {name: len(arr) for name, arr in data.items()}
    unique_lengths = set(lengths.values())
    if len(unique_lengths) != 1:
        raise ValueError(
            f"All feature arrays must have the same length.  Got: {lengths}"
        )
    self._data: dict[str, np.ndarray] = {k: np.asarray(v) for k, v in data.items()}
    n = next(iter(unique_lengths))
    if times is None:
        self._times = np.arange(n, dtype=float)
    else:
        self._times = np.asarray(times, dtype=float)
        if len(self._times) != n:
            raise ValueError(
                f"'times' length ({len(self._times)}) must match feature length ({n})."
            )
    self.sr = float(sr)
    self.source = Path(source) if source is not None else None
    self.metadata: dict[str, Any] = dict(metadata) if metadata else {}

feature_names property

feature_names

Names of the feature channels.

n_features property

n_features

Number of feature channels.

n_samples property

n_samples

Number of time samples per channel.

shape property

shape

(n_features, n_samples).

times property

times

Time axis in seconds.

absolute_times

absolute_times()

Wall-clock time stamps (epoch seconds) for each sample.

Requires metadata["start_datetime"] — a datetime or ISO string, e.g. from musicalgestures._timecode.media_start_datetime.

Source code in musicalgestures/_features.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def absolute_times(self) -> np.ndarray:
    """Wall-clock time stamps (epoch seconds) for each sample.

    Requires ``metadata["start_datetime"]`` — a ``datetime`` or ISO
    string, e.g. from ``musicalgestures._timecode.media_start_datetime``.
    """
    import datetime as _dt

    start = (self.metadata or {}).get("start_datetime")
    if start is None:
        raise ValueError(
            "no metadata['start_datetime']; set it (see "
            "musicalgestures._timecode) to place features on the "
            "wall clock")
    if isinstance(start, str):
        start = _dt.datetime.fromisoformat(start)
    return start.timestamp() + np.asarray(self.times, dtype=float)

__len__

__len__()

Return the number of feature channels.

Source code in musicalgestures/_features.py
154
155
156
def __len__(self) -> int:
    """Return the number of feature channels."""
    return self.n_features

__getitem__

__getitem__(key)

Return a single feature array by name.

Source code in musicalgestures/_features.py
158
159
160
def __getitem__(self, key: str) -> np.ndarray:
    """Return a single feature array by name."""
    return self._data[key]

__iter__

__iter__()

Iterate over feature names.

Source code in musicalgestures/_features.py
165
166
167
def __iter__(self):
    """Iterate over feature names."""
    return iter(self._data)

__array__

__array__(dtype=None, copy=None)

Return a 2-D array of shape (n_features, n_samples).

Source code in musicalgestures/_features.py
169
170
171
172
def __array__(self, dtype=None, copy=None) -> np.ndarray:
    """Return a 2-D array of shape ``(n_features, n_samples)``."""
    arr = np.stack(list(self._data.values()), axis=0)
    return arr if dtype is None else arr.astype(dtype)

to_numpy

to_numpy()

Return all features as a 2-D NumPy array (n_features, n_samples).

Returns:

Type Description
ndarray

Shape (n_features, n_samples). Row order matches :attr:feature_names.

Source code in musicalgestures/_features.py
178
179
180
181
182
183
184
185
186
187
def to_numpy(self) -> np.ndarray:
    """Return all features as a 2-D NumPy array ``(n_features, n_samples)``.

    Returns
    -------
    np.ndarray
        Shape ``(n_features, n_samples)``.  Row order matches
        :attr:`feature_names`.
    """
    return np.array(self)

to_dataframe

to_dataframe()

Return features as a :class:pandas.DataFrame.

Returns:

Type Description
DataFrame

Columns are feature names, index is the time axis in seconds.

Source code in musicalgestures/_features.py
189
190
191
192
193
194
195
196
197
198
199
def to_dataframe(self) -> pd.DataFrame:
    """Return features as a :class:`pandas.DataFrame`.

    Returns
    -------
    pd.DataFrame
        Columns are feature names, index is the time axis in seconds.
    """
    df = pd.DataFrame(self._data, index=self._times)
    df.index.name = "time_s"
    return df

to_json

to_json(path=None)

Serialise to JSON (with metadata).

Parameters:

Name Type Description Default
path str | Path | None

Optional file path to write the JSON to. If None, returns the JSON string.

None

Returns:

Type Description
str

JSON-encoded string representation.

Source code in musicalgestures/_features.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def to_json(self, path: str | Path | None = None) -> str:
    """Serialise to JSON (with metadata).

    Parameters
    ----------
    path:
        Optional file path to write the JSON to.  If *None*, returns
        the JSON string.

    Returns
    -------
    str
        JSON-encoded string representation.
    """
    payload: dict[str, Any] = {
        "source": str(self.source) if self.source else None,
        "sr": self.sr,
        "n_features": self.n_features,
        "n_samples": self.n_samples,
        "feature_names": self.feature_names,
        "times": self._times.tolist(),
        "data": {k: v.tolist() for k, v in self._data.items()},
        "metadata": self.metadata,
    }
    json_str = json.dumps(payload, indent=2)
    if path is not None:
        Path(path).write_text(json_str, encoding="utf-8")
        logger.info("MgFeatures saved to %s", path)
    return json_str

from_json classmethod

from_json(path)

Load an :class:MgFeatures instance from a JSON file.

Parameters:

Name Type Description Default
path str | Path

Path to the JSON file previously created by :meth:to_json.

required

Returns:

Type Description
MgFeatures
Source code in musicalgestures/_features.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
@classmethod
def from_json(cls, path: str | Path) -> "MgFeatures":
    """Load an :class:`MgFeatures` instance from a JSON file.

    Parameters
    ----------
    path:
        Path to the JSON file previously created by :meth:`to_json`.

    Returns
    -------
    MgFeatures
    """
    payload = json.loads(Path(path).read_text(encoding="utf-8"))
    data = {k: np.array(v) for k, v in payload["data"].items()}
    times = np.array(payload["times"])
    return cls(
        data=data,
        times=times,
        sr=payload.get("sr", 1.0),
        source=payload.get("source"),
        metadata=payload.get("metadata", {}),
    )

from_dataframe classmethod

from_dataframe(df, sr=1.0, source=None, metadata=None)

Create an :class:MgFeatures from a :class:pandas.DataFrame.

Parameters:

Name Type Description Default
df DataFrame

DataFrame whose columns are feature names and whose index is the time axis in seconds.

required
sr float

Sampling rate in Hz.

1.0
source str | Path | None

Optional source file path.

None
metadata dict[str, Any] | None

Optional metadata dictionary.

None

Returns:

Type Description
MgFeatures
Source code in musicalgestures/_features.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
@classmethod
def from_dataframe(
    cls,
    df: pd.DataFrame,
    sr: float = 1.0,
    source: str | Path | None = None,
    metadata: dict[str, Any] | None = None,
) -> "MgFeatures":
    """Create an :class:`MgFeatures` from a :class:`pandas.DataFrame`.

    Parameters
    ----------
    df:
        DataFrame whose columns are feature names and whose index is
        the time axis in seconds.
    sr:
        Sampling rate in Hz.
    source:
        Optional source file path.
    metadata:
        Optional metadata dictionary.

    Returns
    -------
    MgFeatures
    """
    data = {col: df[col].to_numpy() for col in df.columns}
    times = df.index.to_numpy(dtype=float)
    return cls(data=data, times=times, sr=sr, source=source, metadata=metadata)