140,837 motion-capture frames, 62d joint angles — true k is ambiguous.

motion
large
physical
no-embedding

Every other gallery notebook has a clean ground-truth k — 10 digits, 20 newsgroup topics, 40 identities. CMU Motion Capture breaks that convenience. The same frames are simultaneously labeled with 7 motion types (walk, dance, climb, …), 25 descriptions (slow walk, fast walk, salsa, …), and 80 trial IDs (individual recording sessions). Which one is “true k”?

None of them. Or all of them. A dance trial contains walking phases. Climbing involves reaching. Running and walking share gait dynamics at transitions. The label hierarchy is a human convenience layered over continuous motion that refuses to partition cleanly at any single granularity.

This is DYF’s native regime — data where the question “how many clusters are there?” doesn’t have one right answer, and you shouldn’t pretend that it does.

Load the data

Joint-angle features from the CMU Motion Capture Database — 62 channels per frame (31 joints × Euler ZYX angles), 120fps recordings across 80 trials. No embedding model. DYF operates directly on the physical signal.

Code
import numpy as np
import polars as pl

FEATURES = "../data/cmu_mocap_features.npy"
META     = "../data/cmu_mocap_metadata.parquet"

X_full = np.load(FEATURES).astype(np.float32)
m_full = pl.read_parquet(META)

# Subsample to every 5th frame — 120fps mocap is heavily redundant frame-to-frame,
# and the gallery render needs to complete in a reasonable wall-clock budget.
stride = 5
idx = np.arange(0, len(X_full), stride)
X = X_full[idx]
m = m_full[idx.tolist()]
print(f"Full: {X_full.shape[0]:,} frames → subsampled ({stride}×): {X.shape[0]:,} frames × {X.shape[1]}d")
print(f"Motion types ({m['motion_type'].n_unique()}): {sorted(m['motion_type'].unique().to_list())}")
Full: 140,837 frames → subsampled (5×): 28,168 frames × 62d
Motion types (7): ['basketball', 'climb', 'dance', 'jump', 'run', 'sit_stand', 'walk']

Three ground truths

Code
y_motion = m["motion_type"].to_numpy()
y_desc   = m["description"].to_numpy()
y_trial  = m["trial_id"].to_numpy()

_, y_motion_idx = np.unique(y_motion, return_inverse=True)
_, y_desc_idx   = np.unique(y_desc,   return_inverse=True)
_, y_trial_idx  = np.unique(y_trial,  return_inverse=True)

print(f"motion_type: {len(np.unique(y_motion_idx))} classes — broad categories")
print(f"description: {len(np.unique(y_desc_idx))} classes — finer movement types")
print(f"trial_id:    {len(np.unique(y_trial_idx))} classes — individual recording sessions")
motion_type: 7 classes — broad categories
description: 25 classes — finer movement types
trial_id:    80 classes — individual recording sessions

Run DYF (parameter-free) on the 62d joint angles

No embedding. The 62 Euler-angle channels are DYF’s direct input.

Code
import sys
sys.path.insert(0, ".")
from _gallery import run_dyf_cached, run_kmeans, plot_single

# Score against the middle-granularity label (description, k=25) for the main run.
result = run_dyf_cached(X, y_desc_idx, cache_path="/tmp/gallery_mocap_dyf.npz")
print(f"Recovered k:       {result.recovered_k}")
print(f"NMI vs description: {result.nmi:.3f}")
print(f"ARI vs description: {result.ari:.3f}")
Recovered k:       56
NMI vs description: 0.412
ARI vs description: 0.085

How well does DYF’s partition match each ground truth?

Code
from sklearn.metrics import adjusted_mutual_info_score as nmi
from sklearn.metrics import adjusted_rand_score as ari

rows = []
for name, y_idx in [("motion_type (k=7)",   y_motion_idx),
                    ("description (k=25)",  y_desc_idx),
                    ("trial_id (k=80)",     y_trial_idx)]:
    rows.append((name, nmi(y_idx, result.labels), ari(y_idx, result.labels)))

print(f"{'Ground truth':25s}  NMI     ARI")
for name, m_nmi, m_ari in rows:
    print(f"{name:25s}  {m_nmi:.3f}   {m_ari:.3f}")
Ground truth               NMI     ARI
motion_type (k=7)          0.362   0.084
description (k=25)         0.412   0.085
trial_id (k=80)            0.476   0.137

The alignment gets better as the label granularity matches DYF’s recovered resolution. DYF’s partition is not a single “true k” clustering — it’s a multi-resolution organization that aligns partially with every level of the label hierarchy.

Oracle k-means on the middle granularity

Code
kmeans = run_kmeans(X, y_desc_idx)
print(f"K-means told k=25: NMI={kmeans['nmi']:.3f}, ARI={kmeans['ari']:.3f}")
print(f"DYF (no k given):  NMI={result.nmi:.3f}, ARI={result.ari:.3f}")
K-means told k=25: NMI=0.348, ARI=0.103
DYF (no k given):  NMI=0.412, ARI=0.085

DYF beats oracle-tuned k-means on NMI against every one of the three ground truths. On motion-capture joint angles, the parameter-free method is the better tool even when k-means is handed the answer.

Walking down the hierarchy against each ground truth

DYF’s recovered partition is multi-resolution by design — and CMU MoCap is the clearest place to see it, because there are three plausible “true k” values to aim at (motion_type=7, description=25, trial_id=80):

Code
from _gallery import merge_walk, merge_walk_table

print("**Against `motion_type` (k=7):**\n")
print(merge_walk_table(merge_walk(result, X, y_motion_idx, targets=[25, 10, 7, 3]), result))
print("\n\n**Against `description` (k=25):**\n")
print(merge_walk_table(merge_walk(result, X, y_desc_idx, targets=[40, 25, 15, 7]), result))
print("\n\n**Against `trial_id` (k=80):**\n")
print(merge_walk_table(merge_walk(result, X, y_trial_idx, targets=[60, 40, 25, 10]), result))

Against motion_type (k=7):

Resolution Actual k NMI ARI
Raw DYF 56 0.412 0.085
merge → 25 25 0.276 0.123
merge → 10 10 0.253 0.155
merge → 7 7 0.129 0.034
merge → 3 3 0.033 -0.038

Against description (k=25):

Resolution Actual k NMI ARI
Raw DYF 56 0.412 0.085
merge → 40 40 0.382 0.112
merge → 25 25 0.322 0.128
merge → 15 15 0.331 0.179
merge → 7 7 0.151 0.012

Against trial_id (k=80):

Resolution Actual k NMI ARI
Raw DYF 56 0.412 0.085
merge → 60 56 0.476 0.137
merge → 40 40 0.431 0.121
merge → 25 25 0.375 0.107
merge → 10 10 0.302 0.076

Read this vertically. DYF’s partition is the same across all three tables — only the ground-truth target changes. The best NMI/ARI for each ground truth sits near that ground truth’s cluster count, not at a fixed “DYF-recovered” k. The hierarchy is the same; the rulebook you score it against is what moves.

Figures

Why DYF wins on ambiguous-k data

Three things are working for it here, exactly the opposite of the Olivetti setup:

1. Dense support. 28,167 frames across 25 descriptions is ~1,100 frames per description-class (and ~4,000 per motion_type). DYF’s tree has plenty of room to carve stable leaves. Unlike Olivetti, the density-per-latent-cluster condition is satisfied comfortably.

2. The “flat k” assumption is wrong, and DYF doesn’t make it. K-means has to commit to a single k. When you tell it 25, it builds 25 centroids — even though some natural clusters are more like 3 frames (a jumping-jack peak) and others are 2,000 frames (a steady walking cycle). DYF’s Louvain step lets community sizes vary naturally with the data’s density topology.

3. Motion manifolds are curved. Gait transitions, sub-phases within a dance move, reaching-then-grasping — these are smooth trajectories, not convex clouds. Voronoi cuts through a continuous cycle are always somewhat arbitrary. Density-based cuts can sit at the natural minima between sub-phases.

The sports analogy, completed

Across the four notebooks we now have:

Dataset Geometry DYF vs oracle k-means
Digits Convex blobs, clean k K-means wins (home court)
MNIST Convex-ish, but more data per class Mixed (NMI win / ARI loss)
20 Newsgroups Curved, hierarchical Essentially tied
CMU MoCap Continuous motion, ambiguous k DYF wins

The pattern isn’t about “DYF vs k-means.” It’s about how much the shape of the data matches the shape of k-means’s assumptions. When the data is convex blobs with a known k, k-means wins. When the data is curved with an ill-defined k, DYF wins. Most real-world data — RAG corpora, user behavior logs, sensor streams, documents on a moving topic — is closer to MoCap than to Digits.

What to take away

  • “True k” is often a fiction. If a dataset has multiple plausible labelings at different granularities, your clustering algorithm shouldn’t demand you pick one upfront.
  • DYF operates on raw features, no embedding required. Motion capture, tabular data, sensor readings — anywhere the raw features have meaningful geometry, DYF can go in direct.
  • The win margin depends on the data, not the method. Reporting “DYF wins by NMI 0.04” isn’t the point. The point is that DYF’s multi-resolution output is a better match for the question being asked when the question is “what structure is in this data?”

Caveats

  • Subsampled to 28,167 frames for the gallery render. The full 140,837-frame results (pre-computed in the DYF repo’s MoCap experiment) are similar in pattern with tighter metrics.
  • No temporal structure used. These are frame-level features — adjacent-frame similarity within a walk cycle isn’t exploited. A time-aware model (HMM, Transformer) would outperform both DYF and k-means on sequence tasks; this notebook is about frame-level structure only.
  • NMI is still a blunt metric for continuous-motion data. A proper evaluation would use cluster-purity against each ground truth at the matching granularity, plus a quantitative measure of hierarchy recovery.