Hugging Face, End to EndLesson 41 of 75

Contents

Hugging Face, End to End

Lesson 41 of 75. Sign in to keep your place.

Module 5 · Datasets, and the work that happens before any model

Transforming a million rows without waiting an hour

9 min read

The naive version, and the fast one

Python
# slow: one Python call per row
ds = ds.map(lambda r: {"n_chars": len(r["text"])})

# fast: one Python call per thousand rows
ds = ds.map(
    lambda b: {"n_chars": [len(t) for t in b["text"]]},
    batched=True, batch_size=1000,
)

In batched mode the function receives a dict of lists and must return a dict of lists of the same length. Ten to fifty times faster is typical, and the reason is not clever: the per-call overhead of crossing between Arrow and Python dominates, and batching pays it a thousand times less often.

For tokenization the gain is larger still, because the Rust tokenizer parallelises across a batch internally:

Python
def tok_fn(batch):
    return tok(batch["text"], truncation=True, max_length=512)

ds = ds.map(tok_fn, batched=True, remove_columns=["text"])

remove_columns drops the original text once you have the ids. On a large corpus this is the difference between a 30 GB cached file and a 6 GB one.

Parallelism across cores

Python
ds = ds.map(fn, batched=True, num_proc=8)

This forks processes, each mapping a shard, and then concatenates. Two cautions that cost people real time:

  • Fast tokenizers already use threads. Combining num_proc with them can oversubscribe the CPU and run slower. The library will warn about TOKENIZERS_PARALLELISM; set it to false when using num_proc, or use one or the other.
  • Anything holding a GPU does not fork well. For GPU work, use one process and a large batch, not eight processes fighting over one card.

A batch does not have to stay the same size

This is the feature people miss. A batched map may return more or fewer rows than it received, which makes it the tool for splitting and dropping:

Python
def chunk(batch, size=800, overlap=100):
    out = {"chunk": [], "doc_id": []}
    for doc_id, text in zip(batch["id"], batch["text"]):
        for i in range(0, len(text), size - overlap):
            out["chunk"].append(text[i:i + size])
            out["doc_id"].append(doc_id)
    return out

chunks = docs.map(chunk, batched=True, remove_columns=docs.column_names)

One thousand documents in, forty thousand chunks out, with the source id carried along. remove_columns=docs.column_names is required here, because the old columns no longer have the right length and the library will refuse the mismatch.

filter, and doing it in one pass

Python
ds = ds.filter(lambda b: [len(t) > 50 for t in b["text"]], batched=True, num_proc=4)

Filtering reads everything, so chain your conditions into one predicate rather than calling filter four times. Four filters are four full passes.

with_transform: when you do not want a new file

map materialises a new Arrow file. When the transformation is cheap and varies per epoch (image augmentation, random cropping, dynamic masking) that is wasteful and wrong, because you want different randomness each time.

Python
def augment(batch):
    batch["pixel_values"] = [aug(img) for img in batch["image"]]
    return batch

ds.set_transform(augment)      # applied on access, nothing written

Rule: map for deterministic preprocessing you want cached, set_transform for anything random or cheap.

Making it reproducible

Every map that involves randomness needs a seed passed in, not read from global state, or your cache and your results diverge from each other. And keep the transformation code in a module rather than a notebook cell, because a notebook cell that was edited and re-run is not a record of what produced your data.

Sorting, grouping and joining

sort, class_encode_column and flatten_indices cover most reshaping. There is no join; for that, convert to pandas or Polars, join, and convert back:

Python
import pandas as pd
from datasets import Dataset
merged = Dataset.from_pandas(ds.to_pandas().merge(other_df, on="id"))

That materialises everything in RAM, so it is fine for a few million rows and not for a corpus. For large joins, DuckDB over the parquet files is the next lesson and the right tool.

Do this now

Take one map you have written per row and rewrite it batched. Time both on 50,000 rows. The number you get is the tax you have been paying on every dataset you have ever processed one row at a time.

Every lesson here is open. Your progress is recorded once you pass the Image Generation, in Practice exam.

Check your understanding

A chunking function returns 40,000 chunks from 1,000 documents inside map(batched=True) and raises an error about mismatched lengths. What is missing?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minQuery a dataset with SQL before downloading it

Stuck on something?Ask Arjun, your AI tutor

How to cite: Addaly (2026) ‘Transforming a million rows without waiting an hour’, in Hugging Face, End to End. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/hugging-face/map-filter-and-batching (Accessed: 13 October 2026).