Six Questions to Ask a LeRobot Dataset Before You Train

Community Article
Published October 8, 2026

The metrics worth computing on a LeRobot dataset before you train a policy or fine-tune a VLA: what each one catches, when to trust it, and where it misleads you.

You recorded 400 teleop episodes last week. You pulled 6,000 more from the Hub because the task looked close to yours. Now you want to fine-tune a VLA.

You can't watch all of it. At 30 seconds per episode, 10,000 episodes is about 83 hours of video. So you scrub a handful, they look fine, and you hit train. Then the policy hesitates at the first grasp.

Somewhere in there is an episode with a frozen wrist camera, one with 11 seconds of dead time before the arm moves, one whose task string is "Hold", and one whose video runs 0.4 seconds behind the actions. None of that is visible in a single episode. It shows up when you compare an episode to its peers.

TL;DR

  • Six questions cover most problems: is it broken, is the motion clean, is it wasting frames, did the robot do what was commanded, can you trust the cameras, can a model learn from the instruction. Start with the cheapest.
  • Almost everything is signal math on action and observation.state. No model, no GPU. Only camera metrics decode video, at about 0.4 s per camera per episode.
  • Compare each episode to its own task group. A pooled score ranks the slowest task as the problem.
  • Correlated metrics share one vote. Five time metrics that fire on the same pause are one problem.
  • The evidence is mixed. Use scores to order a review queue, tag what you want to drop yourself, and validate with a training run.

See it first

This is the panel for the first 23 episodes of T-Rex (zekaiwang/trex_dataset, MIT), a bimanual robot with 22-joint dexterous hands, scored with a dual-arm layout and three cameras. Every panel screenshot in this post comes from that run.

The LeRobot Curation panel's Overview tab

Overview tab. Top row: score histogram with warn and fail lines, verdict counts, episodes per task. Below: outliers and the worst-first ranking with Score, Flags, and Driver. The amber banner is expected: each of these 23 episodes has its own task string, so all are scored against the whole view.

Tab Answers Try this
Overview Which episodes do I look at first? Click a red bar, then open the top row
Motion & Action Is it motion, timing, tracking, or the gripper? Click an arm chip to isolate one arm
Integrity & Coverage Is anything broken? Which tasks are thin? Click the fail bar
Vision Is a camera frozen, blurry, or out of sync? Click a camera chip
Language Is the instruction usable? Click a bar
  • The panel follows the view. Filter the grid and every chart, and the ranking recomputes.
  • Every bar is a filter. Click one and the grid shows only those episodes.
  • The ranking says why. Driver names the group behind the score, Flags counts groups at warn or worse, and an asterisk marks scores normalized against the whole view.
  • Nothing is deleted. The footer tag buttons write ordinary sample tags.

Overview tab: the score histogram, a click on a red bar, the worst-first ranking, and the Task dropdown

Overview tab. Click a red bar in the score histogram to filter the grid, read Score, Flags and Driver in the ranking, and switch normalization groups with the Task dropdown. Footage: HIW-500: Humanoids In-the-Wild (BitRobot/HIW-500-LeRobot), CC BY 4.0.

Pick your metrics

Question Metrics Reads Cost Use when
Is it broken? frame_gaps, length_mismatch, nonfinite_values, too_short, video_window_mismatch, schema_mismatch metadata, Parquet free Always, first
Is the motion clean? sparc, ldlj, sparc_phase action seconds Always
Is it wasting frames? idle_lead_s, idle_trail_s, longest_pause_s, end_motion_ratio, length_z action seconds Human teleop
Did the robot follow the command? track_resid, accel_spike_frac, joint_limit_frac, track_lag_ms action + state seconds Leader-follower rigs
Is the operator fighting the grasp? gripper_flips_per_s, missed_grasp_frac, recovery_count gripper seconds Grasp tasks
Does it agree with its peers? action_divergence, stats_leverage state + action seconds 20+ episodes per task
Can you trust the cameras? blur, exposure_err, clipped_frac, frozen_frac, video_action_lag_ms video ~0.4 s per camera per episode Several sessions or rigs
Can a VLA learn from the instruction? task_missing, task_generic task string free Fine-tuning a VLA
Is it unusual? iforest_score, novelty_knn metric vector free Second-look queue only

Inside a score, every metric is oriented so higher means worse. The tables show each metric's raw direction.

The format

  • A LeRobot v3 dataset stores many episodes per Parquet file and per MP4. Metadata (meta/episodes/) says which rows and which video time window belong to episode N.
  • So "episode N" is a pointer into shared files. Pointers drift without anything crashing.
  • Timestamps are written from the nominal fps, not a logged clock. Sensor-health checks like jitter and clock drift don't apply. You check structure and alignment instead.
  • FiftyOne (1.22+) imports each episode as one sample and reads Parquet rows and video frames on demand.

What you tell the tool

Nothing about your robot is guessed. A 14-dimensional action could be two 7-DoF arms, an arm plus a hand, or end-effector deltas. A silent guess makes every smoothness score wrong in a way you can't see.

You pick Why Without it
State array and action array Which arrays are measured and commanded. They start on observation.state and action when the dataset uses those LeRobot standard names Metrics that read the missing array turn off
What the action means A yes/no question: are the actions absolute joint positions in the state's space? Asked only when both arrays have the same size. If they differ, the action is recorded as not joint positions Tracking, spike, and joint-limit metrics turn off
Robot and joints Single or dual arm, a checkbox for multi-joint hands, then the Arm, Gripper, and Hand joints (a left and a right set for dual arm). Each field is one signal No arm joints: no smoothness, idle, or pause. No gripper joints: no gripper metrics
Gripper open direction Does a high value mean open? Asked once gripper joints are picked Recovery, missed grasps, and gripper phases turned off
Cameras Which video cameras to score. None preselected, because decoding is the slow part Camera metrics turned off

Joints are chips, picked the same way as cameras, each offered by name with its observed range beside it (a 0 to 100 range often marks a gripper). Nothing is grouped for you. If the dataset gives no joint names, joints are listed by position (action[0], action[1], ...), and the range tells them apart. A checklist at the bottom of the form shows which metric families will be scored and what each switched-off family still needs. It never blocks the run.

Compute form, Data tab: joints as chips and the list of what will be scored

Compute form, Data tab. Joints are chips, and the checklist under them says what will be scored and what is still off (here, gripper metrics, because no gripper joints were picked).

Compute form, Data tab: picking arrays and joints as chips

Compute form, Data tab. Confirm the arrays, pick arm and gripper joints (the observed range shows beside each), and finish on the "Will be scored / Off" list.

The metrics

1. Integrity: is it broken?

Integrity has its own verdict, broken or not, and never enters the score. It reads only metadata and Parquet, so run it first.

Episodes as pointers into shared Parquet and MP4 files, and the six integrity checks

Figure 1. A v3 episode is a row range in a shared Parquet file plus a time window in a shared MP4 per camera. Each check catches a pointer that stopped matching the data. Illustrative values.

Check Catches
frame_gaps Non-contiguous or duplicated frame_index. Any nonzero count is broken
length_mismatch Declared length disagrees with the actual Parquet rows
video_window_mismatch Per camera, the video window disagrees with length / fps
nonfinite_values NaN or inf in action or state
too_short One-frame and near-empty episodes
schema_mismatch After merging sources: different shapes, names, fps, camera keys or resolution

The Hugging Face team's write-up on community datasets lists these as recurring defects: single-frame episodes, Parquet files deleted without reindexing, inconsistent action or state dimensions.

timestamp_dev is reported for information only and never affects the verdict, since timestamps come from fps.

2. Motion: is the motion clean?

Reads action over the joints you marked as Arm (and Hand). On leader-follower rigs (SO-100, Koch, ALOHA), that's the operator's hand, not the robot arm.

How the signal is prepared:

  • Each joint is range-normalized before speeds are combined, so units don't matter.
  • Gripper and hand dimensions are left out of arm speed. A 22-joint hand is its own signal and never swamps a 7-joint arm.
  • Low-pass cutoff is min(10 Hz, 0.4 × fps), so 30 fps data isn't analyzed near Nyquist.
  • Fixed-length windows, with a worst-window value kept so one bad stretch isn't averaged away.
Metric Measures Read it Watch out
sparc Spectral arc length of the arm's speed profile: how many corrections the motion contains Near 0 is smooth, very negative is hesitant Responds to stop-and-go, not to white noise
ldlj Log dimensionless jerk Near 0 is smooth Catches noise, but noisier and duration-sensitive: half weight
sparc_phase (opt-in) SPARC inside each gripper-delimited phase Same as sparc Adapted from RINSE, synthetic tests only
jerk_rms, psd_lf_hf RMS jerk; low/high frequency power Review only, never scored Noise-sensitive; unreliable at LeRobot frame rates

SPARC intuition. A clean reach is one speed bump, and its spectrum is compact. Every correction or stop adds frequency content and lengthens the traced path. SPARC is the negative of that length.

Speed profile and spectrum for a smooth, a stop-and-go and a jittery version of the same two reaches, with SPARC and LDLJ values

Figure 2. The same two reaches, moved three ways. SPARC: smooth -2.57, stop-and-go -5.25, jittery -2.56. LDLJ: smooth -8.87, stop-and-go -14.45, jittery -14.71. SPARC separates stop-and-go. LDLJ penalizes both stop-and-go and noise. Synthetic signals at 30 fps; values computed from them.

Motion & Action tab: one histogram per metric, with arm and hand signal chips

Motion & Action tab: one histogram per metric, one colored series per signal (arm_left, arm_right, hand_left, hand_right). The chips isolate a signal, and dashed lines are warning thresholds.

sparc_phase exists because contact is jerky by nature, and scoring the whole episode as one signal counts every grasp as roughness.

3. Time efficiency: is it wasting frames?

The idle threshold is a multiple of the episode's own moving speed, so a slow, careful operator isn't flagged for being slow.

Metric Measures Read it
idle_lead_s Stillness at the start High is dead time before the task. Stores a suggested trim start
idle_trail_s Stillness at the end High is a dead tail. Stores a suggested trim end
longest_pause_s Longest idle run that touches neither end High is a mid-task stall
end_motion_ratio Speed in the last second vs typical moving speed High means the episode was probably cut off
length_z Robust z of duration within the task group Scored as abs(z). Positive is struggle, negative is abandoned

Arm speed over one episode with idle lead, a mid-episode pause and idle trail marked, plus a duration histogram with length_z

Figure 3. Left: one episode's arm speed with the three idle stretches and the keep window they suggest. Right: length_z places a duration against its task group. Synthetic signals.

Two published pipelines treat dead frames as removable: OpenVLA dropped all-zero no-op actions, and SCIZOR removed samples including "manipulation failures, slow motion, and pauses."

4. Tracking and contact: did the robot follow the command?

On a leader-follower rig, action is the leader arm and observation.state is the follower. When they diverge, something physical happened: a collision, a stall, a slipping joint. These metrics only run when you've set the action to joint positions in the state space.

Metric Measures Read it
track_resid Per-joint gap between action and state after lag alignment, with each joint's median offset removed High means the robot isn't reaching the command (slip, collision, stall)
track_lag_ms Command-to-response delay, from argmax_k corr(action[t], state[t+k]) Mostly a hardware property. Flag only episodes far from the median. Never scored
accel_spike_frac Frames where any joint's state acceleration exceeds median + k × MAD, with an absolute floor High means jolts, a proxy for contact
joint_limit_frac (opt-in) Share of (frame, joint) pairs within 2% of a limit High means working near the edge

Action versus state for one joint with a stall, the residual after lag alignment, and the cross-correlation that estimates lag

Figure 4. The follower trails the leader by a few frames (right), then stalls while the command keeps moving. The residual lights up at the stall, and the catch-up produces an acceleration spike. Synthetic signals.

5. Gripper: is the operator fighting the grasp?

Metric Measures Read it
gripper_flips_per_s Open/close transitions per second, with hysteresis so threshold noise doesn't count High is chatter or hesitation
missed_grasp_frac (opt-in) Close commands where the follower closed fully with no stall, so nothing was held High is empty grasps. Heuristic
recovery_count Regrasps: open, then close again near the same place Neutral. Tagged, never penalized, never scored

Three gripper traces: a clean grasp, chatter, and a regrasp recovery, with flips per second

Figure 5. Chatter scores 0.88 flips per second, compared with 0.25 for a clean grasp. The regrasp has more transitions than the clean grasp and is tagged as a recovery instead of penalized. Synthetic signals.

Several clean cycles are often an operator correcting a failed grasp, which is behavior a policy may want to learn. That's why recovery is never penalized.

6. Consistency: does it agree with its peers?

Metric Measures Read it
action_divergence For frames in this episode, the variance of action among the nearest state neighbors in other episodes of the same task group High means a different action from peers in a similar state
stats_leverage (never scored) How many dimensions this episode alone stretches beyond the rest of meta/stats.json High means one episode distorts normalization for everyone

Belkhale et al. (NeurIPS 2023, arXiv:2306.02437) call this action divergence and report that "making actions more consistent tends to increase policy success."

Neighbouring episodes' actions from a similar state, agreeing on the left and contradicting each other on the right

Figure 6. Each arrow is the action another episode took from a state near this one's. Variance: 0.00 on the left, 0.59 on the right. Schematic, in a 2D stand-in for the state space.

7. Camera: can you trust the pixels?

Opt-in, because it decodes video. No model, just pixel and signal arithmetic. Every value is stored per camera and compared with the same camera in other episodes, so a wrist camera is never judged against a top camera. An episode's camera score is its worst camera.

Metric Measures Read it
blur 10th percentile of Laplacian variance over 12 frames, at a fixed size (longer side 480 px), as log10(1 + variance) Lower is blurrier
exposure_err Mean luminance's distance from mid-gray, 0 to 1 High is too dark or too bright
clipped_frac Share of pixels at pure black or white High is blown highlights or crushed shadows
frozen_frac Of 6 short bursts, the share with near-identical frames while state shows the robot moving High is a frozen or dropped feed
video_action_lag_ms Offset between video motion and arm speed, from cross-correlation over the central 30 s Near 0 is in sync

Five synthetic camera frames showing reference, blur, exposure, clipping and frozen-feed values

Figure 7. The frame metrics on a synthetic scene, computed at the tile's resolution. The plugin measures at a fixed size and compares each camera with itself across episodes. A live camera on a static scene still shows frame-to-frame change; a frozen feed shows none.

  • frozen_frac: a still scene with a still robot isn't a frozen camera, so a burst only counts when the robot is moving and the pixels aren't. A live camera on a static scene typically shows 0.3 gray levels of frame-to-frame change or more (the synthetic scene in Figure 7 shows 0.8), so the 0.05 threshold sits well below it.
  • video_action_lag_ms: the one integrity can't catch. If video runs 400 ms behind actions, every file is intact, and the durations still match, but the policy learns to predict from a stale observation. The value is relative to peers, so the baseline is the lag every episode shares. Nothing is reported when video doesn't track the action (correlation under 0.3).

Arm speed and video motion energy offset in time, the cross-correlation peak, and a histogram of lag across episodes

Figure 8. Video motion energy trails arm speed by 400 ms here. Across episodes from one camera, the shared lag is the baseline, and the differing episodes stand out. Synthetic signals.

Vision tab: five camera metric histograms with camera chips

Vision tab: the five camera metrics with one colored series per camera, and a worst-first table over just those metrics.

Vision tab: camera chips, histograms and a flagged episode in the viewer

Vision tab. Compute camera metrics, isolate one camera with its chip, click a histogram bar to filter the grid, and open a flagged episode in the viewer. Footage: HIW-500: Humanoids In-the-Wild (BitRobot/HIW-500-LeRobot), CC BY 4.0.

Cost. The three frame metrics share one decode of 12 frames. The lag pass costs about 0.9 s per episode across all cameras on the development set. Budget about 0.4 s per camera per episode.

8. Language and outliers

Metric Measures Note
task_missing Empty, whitespace-only or null task string Own verdict, never scored
task_generic Fewer than 3 tokens, a placeholder ("task desc", "Hold"), or no action verb Own verdict, never scored
iforest_score, novelty_knn Isolation-forest score and mean kNN distance over the episode's metric vector, within its task group Never scored

Task strings are canonicalized (case, whitespace, punctuation) before grouping. Language flags get their own verdict because a policy trained without language can ignore them, while a VLA user reads them next to the score. We haven't measured whether generic instructions hurt VLA grounding in anything we reviewed. The check exists because the Hugging Face team lists them among the recurring defects in community datasets.

Outliers never score. The length-confound audit (arXiv:2606.10229, more on it in the next section) found isolation-forest curation matched no curation, and a "weird" episode may be your most valuable edge case.

Evidence at a glance

Metric family Source What it reports Caveat
Smoothness RINSE, arXiv:2604.23000 SPARC filtering gave 16% higher RoboMimic success with one-sixth of the data. SPARC is about 10× more noise-robust than LDLJ Behavior cloning, proprioceptive signals, episodes already filtered for success
Length confound Curation-metrics audit, arXiv:2606.10229 5 of 7 metrics exploited episode length. Detection accuracy and policy success were uncorrelated One preprint, 80 scripted demonstrations
Dead frames OpenVLA, SCIZOR (arXiv:2505.22626) OpenVLA removed all-zero no-op actions. SCIZOR removed samples including "manipulation failures, slow motion, and pauses" Data pipelines, not controlled tests of each metric
Consistency Belkhale et al., arXiv:2306.02437 More consistent actions tend to raise policy success Similar states can need different actions
Stalls, limits Teleoperation paper, arXiv:2605.26349 Flags stalls and operation near joint limits User study had 3 operators
Timing misalignment RoboDrop, arXiv:2609.10021 Observation-action misalignment is a major corruption that action-only methods handle poorly Motivates the lag metric, doesn't validate it
Gripper, tracking none No published downstream validation Treat as sanity checks

From metrics to a ranking

A ranking needs two decisions: compared to what, and how to combine them.

Compared to what. Episodes are grouped by canonicalized task. Groups with 20 or more episodes are scored against themselves. Smaller groups are pooled over the view and marked low-confidence in the panel. Scores are robust z-scores with a floored scale, and zero-inflated metrics (like idle_lead_s) use a tail-based scale, because a standard deviation over mostly zeros is meaningless.

Pooled versus per-task warn lines on the same two tasks

Figure 9. A slower, smaller task (B) pooled with a larger one (A). Pooled, the warn line sits near A's distribution and flags 26 of B's 30 episodes for being task B. Per task: 2 of 120 in A and 1 of 30 in B. Synthetic data, robust z with a median and MAD scale.

Combined how. Correlated metrics share a group, and a group is one vote.

Group Scored members
motion sparc, ldlj, sparc_phase
time idle_lead_s, idle_trail_s, longest_pause_s, end_motion_ratio, length_z
tracking track_resid, accel_spike_frac, joint_limit_frac
gripper gripper_flips_per_s, missed_grasp_frac
consistency action_divergence
camera (opt-in) blur, exposure_err, clipped_frac, frozen_frac, video_action_lag_ms
  • Inside a group, the value is the maximum weighted z. Weights are 1.0, except ldlj, idle_trail_s and end_motion_ratio at 0.5.
  • The episode score is the maximum group value, so one severe problem can't be averaged away by five fine ones. A frozen camera with perfect motion still ranks near the top.
  • n_flags counts groups at or above the warn line (z = 2). driver names the group behind the score.

Group values for an example episode, with the camera group setting the score

Figure 10. An example episode with made-up z-scores. Every group is fine except camera, where frozen_frac is at 4.3.

Quick start

  1. Get a dataset locally. The plugin reads local LeRobot v3 datasets, so download Hub datasets first:

    pip install "fiftyone>=1.22"
    export VFF_MULTIMODAL=1   # enables the multimodal episode viewer; set it in every process that touches the dataset
    hf download <user>/<dataset> --repo-type dataset --local-dir /data/lerobot/my_dataset
    

    A v2.x dataset needs converting first with lerobot's convert_dataset_v21_to_v30 script; the importer rejects it otherwise.

  2. Import it. One sample is one episode.

    import fiftyone as fo
    
    dataset = fo.Dataset.from_dir(
        dataset_dir="/data/lerobot/my_dataset",
        dataset_type=fo.types.LeRobotDataset,
    )
    dataset.add_dir(  # optional: curate across datasets
        dataset_dir="/data/lerobot/other_dataset",
        dataset_type=fo.types.LeRobotDataset,
    )
    session = fo.launch_app(dataset)
    
  3. Compute quality. Install the plugin with fiftyone plugins download https://github.com/harpreetsahota204/lerobot_data_curation, open the operator browser, and run LeRobot curation: compute quality. It runs delegated, in the background. Make your picks on the Data tab, optionally pick cameras on the Camera tab, then run. It remembers your picks for the next run.

  4. Read the ranking. Open a panel and choose LeRobot Curation. It re-ranks as you filter the grid. Use the driver column to pick a tab, and click any bar to filter the samples.

  5. Inspect. Click a row to see every metric with its per-arm or per-camera breakdown, joint traces, speed profile, gripper timeline, and frames. Flagged spans become temporal tags on the episode timeline.

Inspector for one episode

The inspector for the top-ranked episode: score, driver and verdicts, flagged spans as chips, every metric with its per-arm breakdown, and joint traces.

Inspector for one episode, then the viewer timeline with temporal tags

Inspector. Flagged-span chips, per-arm metrics, joint traces, and the gripper timeline, then the same spans as temporal tags on the multimodal viewer timeline. Footage: HIW-500: Humanoids In-the-Wild (BitRobot/HIW-500-LeRobot), CC BY 4.0.

  1. Tag, then export. The footer tags review, exclude-candidate and relabel. Nothing is deleted, and marking exclude-candidate is always your call.

    keep = dataset.match_tags("exclude-candidate", bool=False)
    keep.export(
        export_dir="/data/lerobot/my_dataset_curated",
        dataset_type=fo.types.LeRobotDataset,
        export_media=True,
    )
    

    The native exporter writes a self-contained v3 dataset and renumbers episodes. export_media=True is required. Push it to the Hub like any LeRobot dataset.

Tagging episodes in the panel and exporting the rest

Tag episodes exclude-candidate in the panel, then export the rest as a LeRobot dataset from a notebook.

What the plugin doesn't do

It's heuristic-only. These model-based methods need a model, a GPU, or a policy, so they aren't included. FiftyOne has the embeddings and similarity and model zoo infrastructure to build them as extra operators.

Idea Method What it reports Catch
Instruction matches the video VLM judge. SmolVLA's pipeline rewrote task strings with Qwen2.5-VL-3B from sample frames A rewrite under 30 characters, starting with an action verb VLM judges are noisy, and OpenGVL found open VLMs lag proprietary ones
Near-duplicates Embedding similarity. SCIZOR, arXiv:2505.22626 Two thresholds (εs = 0.58, εd = 0.99) transferred across RoboMimic, Open X-Embodiment and real data Repetition may be what a robust policy needs
Task progress Order shuffled frames by progress (GVL, OpenGVL, TOPReward) TOPReward: VOC 0.874 with Qwen3-VL-8B vs 0.218 for GVL on a filtered Open X-Embodiment subset Model-dependent, authors' own benchmarks
Information content DemInf, arXiv:2502.08623 5-10% improvement on RoboMimic, better on real ALOHA and Franka Works best with relative actions
Policy in the loop CUPID, arXiv:2506.19121; Demo-SCORE CUPID: under 33% of curated data gives state-of-the-art RoboMimic diffusion policies. Demo-SCORE: over 15-35% higher absolute success Needs a policy and rollouts

CUPID's own warning applies to every heuristic here: "human perception of demonstration quality does not necessarily correspond to data that maximizes downstream policy success."

Dataset-level questions aren't per-episode either: task coverage (1,900 episodes of one task and 12 of another isn't balanced), source balance, diversity, and mixture weights (Re-Mix, Hejna et al., CoRL 2024, reports beating uniform weights by 38%). The panel shows coverage as a count on the Integrity & Coverage tab.

Keep it honest

  • Scores order a review queue. Use them to decide where to look, never as an automatic accept/reject gate.
  • The evidence is mixed. Smoothness filtering helped in one study. Another found most metrics exploited episode length. CUPID found data humans rate highly can underperform data chosen by influence on the policy.
  • Thresholds are starting points. The 2% joint-limit margin, the warn line at z ≥ 2, the 20-episode group size, and the camera thresholds were calibrated on a 102-episode development set: the first two episodes from each of the 51 sources in Voxel51/community_v3_10per_embodiment. They aren't adjustable in the form yet. Check them on your data.
  • What the checks prove. Each scored metric must catch a corruption (a stalled joint, a frozen video, a blanked task) on real episodes, and every metric is re-tested after truncating episodes to a common length. On 102 episodes, that takes about 5 minutes. It shows a metric detects what it claims to. It doesn't show that detection predicts policy quality.
  • Validate. Before you drop data, train on the curated subset and on the full set and compare.

Links

Community

Sign up or log in to comment