File size: 3,285 Bytes
37aeb1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"""Interpretability & error-analysis helpers (Plan E).

Pure, torch-free functions over a note DataFrame that carries the ground-truth
``velocity`` and a model prediction column (default ``pred``). Kept separate from
plotting/model code so the aggregation logic is unit-testable.
"""
from __future__ import annotations

import numpy as np
import pandas as pd

ON_BEAT_TOL = 0.1                 # |phase_beat - nearest pulse| below this = "on-beat"


def add_metrical_cols(df: pd.DataFrame, beats_per_bar: int = 4) -> pd.DataFrame:
    """Derive beat position from the phase features (E-GMD test data is all 4/4).

    Adds: ``beat_in_bar`` (0-indexed), ``is_downbeat`` (beat 1), ``is_backbeat``
    (beats 2 & 4), ``metrical_class`` ("on-beat"/"off-beat" by proximity of
    ``phase_beat`` to an integer pulse).
    """
    out = df.copy()
    beat = np.floor(out["phase_bar"].to_numpy() * beats_per_bar).astype(int)
    beat = np.clip(beat, 0, beats_per_bar - 1)
    out["beat_in_bar"] = beat
    out["is_downbeat"] = beat == 0
    out["is_backbeat"] = np.isin(beat, [1, 3])            # 0-indexed beats 2 & 4
    pb = out["phase_beat"].to_numpy()
    dist = np.minimum(pb, 1.0 - pb)                       # distance to nearest pulse
    out["metrical_class"] = np.where(dist < ON_BEAT_TOL, "on-beat", "off-beat")
    return out


def residual_table(df: pd.DataFrame, by, pred_col: str = "pred") -> pd.DataFrame:
    """Per-group error decomposition. Returns a frame indexed by the group key with
    n, true_mean, pred_mean, true_std, pred_std, bias (pred-true), and mae."""
    tmp = df.assign(_ae=(df[pred_col] - df["velocity"]).abs())
    g = tmp.groupby(by)
    res = pd.DataFrame({
        "n": g.size(),
        "true_mean": g["velocity"].mean(),
        "pred_mean": g[pred_col].mean(),
        "true_std": g["velocity"].std(),
        "pred_std": g[pred_col].std(),
        "mae": g["_ae"].mean(),
    })
    res["bias"] = res["pred_mean"] - res["true_mean"]
    return res


def dynamic_level_table(df: pd.DataFrame, pred_col: str = "pred", n_bins: int = 8,
                        lo: float = 0.0, hi: float = 127.0) -> pd.DataFrame:
    """Bin notes by TRUE velocity and report mean prediction + bias per bin.

    Exposes regression-to-the-mean: a point estimator predicts soft notes too loud
    (bias > 0) and loud notes too soft (bias < 0)."""
    edges = np.linspace(lo, hi, n_bins + 1)
    b = np.clip(np.digitize(df["velocity"].to_numpy(), edges[1:-1]), 0, n_bins - 1)
    tmp = df.assign(_bin=b, _ae=(df[pred_col] - df["velocity"]).abs())
    g = tmp.groupby("_bin")
    res = pd.DataFrame({
        "n": g.size(),
        "true_mean": g["velocity"].mean(),
        "pred_mean": g[pred_col].mean(),
        "mae": g["_ae"].mean(),
    })
    res["bias"] = res["pred_mean"] - res["true_mean"]
    res["bin_lo"] = edges[:-1][res.index]
    res["bin_hi"] = edges[1:][res.index]
    return res


def embedding_2d(weight, n_components: int = 2) -> np.ndarray:
    """PCA-project embedding rows to 2D via mean-centered SVD (deterministic).

    weight: array [n, d]. Returns [n, n_components]."""
    W = np.asarray(weight, dtype=float)
    Wc = W - W.mean(axis=0, keepdims=True)
    _, _, Vt = np.linalg.svd(Wc, full_matrices=False)
    return Wc @ Vt[:n_components].T