30 min

The data engine

A demonstration dataset is not a pile of recordings; it is the specification of everything the robot will ever be competent at, written one episode at a time.

Where you are. You can build a scene, drive a simulated arm and record demonstration episodes. This lesson is about what makes a recording worth training on, which is a different question from whether it recorded.

The week you spent on the wrong half

You have probably had this week. A model that used to work stops working. You try a bigger encoder, three learning rates, a different optimiser. Nothing moves. On Friday, out of ideas, you open the training data.

A third of the rows arrived during one week when the upstream service was retrying, so they are duplicates. Another slice was labelled by whoever was on rota, using a dropdown whose default happened to be the most common class. You drop the duplicates, drop the rota week, retrain the original model unchanged, and it beats everything you tried.

Nothing in that story is specific to machine learning. It is the oldest rule in data engineering. What changes in robotics is who caused it. There is no upstream service and no rota. You collected the data, with your own hands, one attempt at a time, and every defect in it is a decision you made without noticing.

The idea in one paragraph

A demonstration dataset is not a record of what happened; it is the specification of what the robot will be able to do. Everything inside the region your demonstrations covered, the policy has a chance at. Everything outside it, the policy has never seen and has no reason to handle. That makes dataset design an engineering activity with its own review checklist, not a chore you do before the interesting part. And the economics are unusual: you cannot scrape more robot data off the internet, so the unit of investment is not GPU-hours but minutes of a human being’s hands. A working policy for a small arm is built from a few dozen episodes and a few minutes of teleoperation. When the numbers are that small, one careless half hour of recording is a large fraction of everything the robot knows.

What is actually in an episode

One episode 203 frames · 6.8 s · 30 fps one frame, one row of the dataset observation.state [86.11, -33.00, 34.65, 88.89, -33.14, 6.98] action [83.97, -34.23, 32.82, 89.12, -33.09, 1.55] timestamp 4.0 s · frame_index 120 · episode_index 7 task_index 0 · "pink lego brick into the transparent box" the camera streams stay in MP4 files; the timestamp is what indexes them where it was vs what it was told
One episode as a bar of frames, with a single frame magnified into the row it becomes in the dataset

Wider than the screen; scroll it sideways.

Each frame carries two vectors of the same shape. observation.state is what the arm reported about itself: six joint angles, read from the encoders. action is what it was told to do at that instant: six target joint angles, which is exactly the interface the servos accept and nothing more. The camera frames live in separate video files, indexed by the timestamp.

The contract between those columns is the single easiest thing to get wrong. The action stored at frame tt is the command that carried the robot from the state at tt to the state at t+1t+1. Off by one in either direction and you are training a policy to predict what already happened.

Formally, the thing you are building is a set of observation-action pairs drawn from whatever the demonstrator happened to do:

D={(ot(i),at(i))  :  i=1N,  t=0Ti1}\mathcal{D} = \left\{ \left(o^{(i)}_t,\, a^{(i)}_t\right) \;:\; i = 1 \dots N,\; t = 0 \dots T_i - 1 \right\}

which reads: for each of the NN episodes, and each frame in it, one pair of what was seen and what was done. Behaviour cloning, two lessons from now, is nothing more elaborate than fitting a function to that set. Which is why everything interesting about the result is decided here, before any training runs.

Coverage is the currency

Volume is the number that feels like progress. Coverage is the one that predicts success.

Fifty times the same episode 50 episodes · 12,000 frames · one starting position Fifty different episodes 50 episodes · 12,000 frames · the whole mat Each dot is where the brick started. Same frame count; only one of these says where the brick can be.
Two demonstration sets of the same size, one with every episode starting from the same spot and one spread across the whole mat

Wider than the screen; scroll it sideways.

Two datasets, both fifty episodes, both twelve thousand frames. In the first, the brick starts in the same place every time. In the second, it starts anywhere on the mat. They cost the same to record and they are not remotely the same dataset: the first one has taught the policy one trajectory very well and told it nothing at all about where the brick can be.

The honest statement of what a dataset taught is the range each joint actually visited. Here is that range, measured on all 11,939 frames of the real dataset:

image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/ −100 −50 0 50 100 commanded joint position (percent of calibrated travel) gripper wrist roll wrist flex elbow flex shoulder lift shoulder pan 181 108 87 66 73 33 span
Per-joint span of the commanded action across the whole dataset, against each joint's full calibrated travel

Two things jump out, and both are the kind of thing you want to know before you spend an evening training. The shoulder pan swept 181 of its roughly 200 available units, so the policy has seen the arm point almost anywhere. The gripper never went past 33, on a scale where two other joints reach exactly 100. Whatever this policy learns about grasping, it has never once seen the fingers open wide.

The other half of coverage is where episodes begin, because that is the distribution the robot will be reset into. On this dataset the shoulder pan starts anywhere across 19 units and the wrist roll across 21, but the elbow starts within 0.63 units every single time. Every demonstration begins from essentially one elbow angle. That is not a defect - it is a decision, made implicitly by whoever set up the rig - but it is a decision the dataset makes on your behalf and does not tell you about unless you look.

Consistency, and the cost of a sloppy hour

Coverage says which situations are in the dataset. Consistency says whether the answers inside it agree.

The same fifty episodes run from 183 to 306 frames, so the longest attempt takes 1.67 times as long as the shortest. Some of that is genuine task variation. Some of it is a human being who was quicker before lunch.

And 14.2 percent of all frames carry no new command at all: the action is bit-identical to the previous frame’s. The median episode has three such frames at the front before anything happens. The worst has 76, which is two and a half seconds of a recording light being on while nobody moved.

Trimming is a judgement call, not a rule. A short settle at the start of an episode is often deliberate, giving the physics or the exposure time to settle. Two and a half seconds is not that. The point of the audit is to make you look, decide, and write down what you decided.

Design the episode before you record it

Almost every dataset problem is a specification problem wearing a data costume. So write the specification first. Six questions, and the answers are the design:

DecisionWhat goes wrong if you skip it
What varies between episodes, and over what rangeThe variation you never recorded is the variation the policy cannot handle. This is the single highest-leverage line in the table.
What is deliberately held fixedFine to fix things. Not fine to discover in Module 4 that the lighting was constant and you did not notice.
Where the episode starts and stopsRagged endings teach the policy that stopping is optional. Long dead heads teach it that waiting is a valid move.
What counts as success, decided before recordingDeciding afterwards is how a 60 percent result becomes an 80 percent result with no code changing.
Whether failed attempts are kept, and how they are markedSilently dropping failures is defensible and common. Silently keeping them is not, and either way the reader of your results needs to know which you did.
One task or several, and the exact sentenceThe task string is a training input for anything language-conditioned. “Pink lego brick into the transparent box” and “put the brick away” are different labels for the same motion.

The format, and why it is boring on purpose

meta/ info.json schema, fps, path templates tasks.parquet the task sentence, by id episodes/chunk-000/file-000.parquet length and row range of each episode data/ chunk-000/file-000.parquet 11,939 rows all 50 episodes, one file one row per frame episode 7 is rows 1652 to 1854 of it videos/ images.up/chunk-000/file-000.mp4 images.side/chunk-000/file-000.mp4 one file per camera, not per episode two AV1 streams, 480 x 640 The previous format wrote one Parquet and one MP4 per episode. Version 3 packs many episodes into each file and resolves the boundaries through the metadata, because file systems buckle long before robot fleets do.
The three directories of a LeRobotDataset version 3: metadata, one Parquet file of all frames, and one MP4 per camera

Wider than the screen; scroll it sideways.

The format your recorder writes, and the format the whole open robot-learning ecosystem reads, is LeRobotDataset. It is Parquet files, JSON metadata and MP4 video in a directory. That is the entire idea, and the boringness is the feature: the audit script in this lesson reads a published 50-episode dataset with pandas and huggingface_hub and nothing else. Your data outlives whichever training framework is fashionable this quarter, as long as you keep it in something a dataframe can open.

Why it is called an engine

One last thing, and it is the reason this lesson is first rather than last. Collecting data is not a step you complete. You record, you train, you deploy, you watch where it fails, and you record more demonstrations in exactly that region. Then again.

That loop is the whole shape of modern robot learning, and it is a direct consequence of the problem you will meet in the covariate-shift lesson: a policy generates its own test distribution, so the places it needs data are places you cannot know until it has run. A dataset built once, in one sitting, is a snapshot of your guesses about which situations matter. The engine is what turns those guesses into measurements.

Check yourself

1. Your policy learns to do the task perfectly when the brick starts in the middle of the mat and fails at the edges. You have 50 episodes. What is the cheapest fix, and what is the fix that will not work?

Record more episodes with the brick near the edges. The coverage report tells you the edges are outside the region your demonstrations visited, and no amount of training on the existing 50 episodes changes which situations are in them. The fix that will not work is a bigger network or a longer training run: both make the policy more confident about the region it already has, and neither invents data about the edges.

2. Why is action never equal to observation.state in a teleoperated dataset, and what happens if you train on the wrong one?

The action is the command the leader arm sent; the state is where the follower actually was when the frame was captured. The follower is always chasing, so the two differ - on this dataset by 1.7 to 4.1 units per joint. Train a network to predict the state from the state and it learns the identity function, which produces a low loss and a policy that commands the pose it is already in, so the arm never moves. This is the most common way to get an excellent loss curve and a dead robot.

3. Two datasets have identical frame counts, identical episode counts and identical file sizes. What single measurement most reliably tells you which one will train a better policy?

The spread of the starting conditions, and after that the visited range of each joint. Frame count measures how much you recorded; coverage measures how many distinct situations you recorded. Fifty repetitions of one situation and fifty different situations cost the same to store and are not comparable as training data.

4. Version 3 of the dataset format packs many episodes into one Parquet file instead of one file per episode. What does that buy, and what does it cost you as a reader of the data?

It buys scale: one file per episode means a million small files for a large fleet, which is where file systems and object stores start to hurt. It costs you legibility. Episode boundaries are no longer visible in the directory listing; they live in meta/episodes/ as row ranges, so any tool that reads the data must read the metadata too. A script that assumes one file is one episode will silently treat fifty demonstrations as one.

5. You inherit a dataset where 30 percent of frames carry an action identical to the previous frame. Name one innocent explanation and one that should worry you.

Innocent: the task genuinely has stationary phases - waiting for a part to settle, holding a grasp - and the operator correctly did nothing. Worrying: the recording was left running between attempts, or the teleoperation link dropped commands, so a third of your training signal is “whatever you were doing, keep doing it” attached to observations that had nothing to do with waiting. The two look identical in the summary statistic, which is why you look at where in the episode the frozen frames sit.

6. Why does the argument “just collect more data” work differently in robotics than it does for text or images?

Because there is no corpus to collect from. Every frame was produced by a person moving a physical arm in real time, so the dataset grows at the speed of human wall-clock and stops growing when they get tired. Fifty episodes is roughly six minutes of teleoperation, which is why the discipline moved toward getting more out of small datasets - predicting whole action sequences at once, generating actions rather than averaging them, and starting from a model somebody else already trained - rather than toward scale alone.

Do this

Audit a real dataset before you ever train on it. code/dataset_audit.py has three TODO(you) functions; code/demos.py is given to you complete and does the reading.

pip install numpy pandas pyarrow huggingface_hub
python dataset_audit.py                      # 50 real SO-101 episodes, from the Hub
python dataset_audit.py --path pick_demos    # your own Module 2 recording

It downloads about 450 kilobytes - the metadata and the numeric columns only, never the 80 megabytes of video - and finishes in a few seconds. When your three functions are right, the report reproduces these measured numbers:

what the audit askswhat this dataset answers
episode length, median and ratio230 frames (7.7 s), longest / shortest = 1.67x
widest joint coverageshoulder pan, 181 units of about 200
narrowest joint coveragegripper, 33 units
frames carrying no new command14.2%
dead air at episode startmedian 3 frames, worst 76 (2.5 s)
tightest start-state spreadelbow flex, 0.63 units across all 50 episodes
mean gap between action and state1.7 to 4.1 units per joint

All seven rows were measured on 2026-08-09 over every one of the 11,939 frames, not sampled.

Then do the part that matters:

  1. Run it against your own Module 2 recording. Your scripted expert is perfectly consistent and never gets tired, so its numbers should look very different from a human’s. Write down which of the seven rows differ and why. If your dead-air number is high, find out whether that is your settle phase or a bug.
  2. Break something on purpose. Truncate one episode’s rows without updating the length in the metadata, and confirm episode_lengths raises rather than returning a plausible-looking number. A cross-check that never fires has not been tested.
  3. Add an eighth row to the report. A good one: for each episode, the distance between its starting state and the mean starting state, so you can see at a glance whether any episode started somewhere unusual. You will want this again in the evaluation lesson, where “which conditions did you actually test” becomes the whole argument.

What you can now do

You can read a LeRobot-format dataset with nothing but a dataframe library, and say what is in it: how long the episodes are, how much they disagree, which joint ranges were visited and which never were, how much of the recording is dead air, and how tightly the starting poses cluster. You can state the frame contract that ties an action to the state it produced, and explain why confusing the action column with the state column yields an excellent loss curve and a motionless robot. And you can write the six-line specification for an episode before you record a single one, which is the difference between a dataset that says what you meant and a dataset that merely exists.

What you can now do

You can audit a demonstration dataset for coverage, consistency and dead air before training anything on it, and design an episode so the dataset says what you meant it to say.