QuiddityML

By · 24 October 2026 · 7 min read

How to build your own NLP dataset: selecting data, extracting text, documenting it

How to choose the text for a custom dataset and pull clean text out of web pages, PDFs and scans, plus what to write down so the next person can trust it.

Building your own NLP dataset means collecting the text for a task yourself, labeling it, and writing down how it was made. You do this when no public dataset matches your task, for example a classifier for the support tickets of your own product or for radiology reports from one hospital. It takes more effort than reusing a public dataset, and each step has its own ways to go wrong.

The steps, in order

Building a dataset runs through six steps:

  1. Choose the text to collect.
  2. Extract clean text from messy sources such as web pages and PDFs.
  3. Define the labels, the exact set of allowed labels and what each one means.
  4. Annotate, which means attaching those labels to each example, yourself or with other people.
  5. Measure agreement, whether different people give the same example the same label.
  6. Document it, so the next person knows what they are holding.

A decision diamond asking whether a public dataset fits: yes leads to reuse, and no leads down six steps, choose the text, extract clean text, define the labels, annotate, measure agreement, document it

Done well, these steps give a dataset that teaches a model the task. Skipped, they give one that teaches it the wrong thing. This post covers choosing the text, extracting it and documenting it. Labels and agreement get a post of their own.

Selecting data: the task decides the text

The application decides what text belongs in the dataset. A classifier that reads radiology reports has to be built from radiology reports. Train it on news articles and it will not know the vocabulary, the abbreviations or the structure of the reports it meets once deployed.

A general-purpose system is the opposite case. So that it does not get tuned to one narrow kind of language, it learns from a corpus spread across many genres and styles on purpose. A corpus is a body of text gathered for training or study. The Brown corpus is a classic example of a wide one. It was built from newspaper reporting, academic prose, fiction and more. The narrower your task, the narrower your data can be, and a broad task needs a wide sample.

A narrow task where radiology reports feed a classifier and news articles are crossed out for the wrong vocabulary, above a general-purpose corpus built from press, academic, fiction, government and popular lore text

Sample the target population

The target population is all the text your model will see once it is deployed. Every dataset is a sample from that population, and a model trained on it works well only if the sample looks like the text it will see.

A common mistake is to sample whatever text is easiest to grab. Say you are building a classifier that sorts customer-support messages by intent, and you train it on movie reviews because that dataset was handy. Support messages are short, full of product terms and often frustrated, and movie reviews are none of those things. The model learns patterns from reviews and fails on real support messages.

The target population of support messages with your sample inside it, and movie reviews grabbed because they were handy pointing at it with a red cross, because support text is short, full of product terms and often frustrated

The time range, the mix of sources and any filtering you apply all shape what the model can learn. For the support classifier, that means messages from recent months and from every channel customers use. It also means no filter that drops the angry ones. Make each of these choices on purpose rather than by convenience.

Extracting text from web pages

Raw sources rarely hand you clean text. A web page wraps the words you want in HTML tags, scripts and styling. Getting the text out takes two steps.

First you parse the HTML and strip the tags. BeautifulSoup (pip install beautifulsoup4) does this with get_text:

1from bs4 import BeautifulSoup
2 
3html = """
4<div class="page">
5  <nav id="menu">
6    <ul>
7      <li><a href="/">Home</a></li>
8      <li><a href="/about">About</a></li>
9      <li><a href="/contact">Contact</a></li>
10    </ul>
11  </nav>
12  <div class="content">
13    <h1>Ten ways to speed up your code</h1>
14    <p>Writing fast code is not just about clever algorithms.</p>
15  </div>
16  <div class="sidebar">
17    <div class="ad"><script src="ads.js"></script>Buy now</div>
18  </div>
19  <footer>© 2024 Example Inc.</footer>
20  <script src="analytics.js"></script>
21</div>
22"""
23 
24soup = BeautifulSoup(html, "html.parser")
25print(soup.get_text(" ", strip=True))
1Home About Contact Ten ways to speed up your code Writing fast code is not just about clever algorithms. Buy now © 2024 Example Inc.

The tags are gone, and the menu, the ad and the copyright line are still there. The second step is boilerplate removal: dropping navigation menus, ads, cookie banners and footers so only the main content is left.

1for tag in soup(["nav", "footer", "script"]):
2    tag.decompose()
3for tag in soup.select(".sidebar"):
4    tag.decompose()
5print(soup.get_text(" ", strip=True))
1Ten ways to speed up your code Writing fast code is not just about clever algorithms.

Skip this step on a scrape of a whole site and every page adds the same menu and footer text. The model then learns the site's page template instead of what the pages say.

A web page's HTML with a menu, an ad script, a footer and an analytics script around one heading and paragraph, and below it the recovered main content after stripping tags and dropping boilerplate

Real sites name their menus and sidebars differently, so the tags and class names to drop change from site to site. Print a few extracted pages and read them before you extract thousands.

PDFs, scans and encodings

PDFs are harder than web pages. A PDF stores where each glyph (each drawn character) sits on the page, not the order of the words. Pulling text out can scramble the reading order, merge two columns into one, or split words across lines. Extraction tools help, and the output usually still needs cleanup.

Scanned documents and photos hold no text at all, only pixels. Optical character recognition (OCR) reads characters from those pixels. It makes recognition errors, so "rn" can come out as "m" and a smudge can turn into a random character.

The last problem is the character encoding. Text is stored as bytes, and an encoding such as UTF-8 is the rule that turns those bytes back into characters. Read the bytes with the wrong rule and accented or non-Latin characters come out as garbage:

1raw = "€ a".encode("utf-8")
2print(raw.hex(" "))
3 
4print(raw.decode("utf-8"))
5print(raw.decode("cp1252"))
6print(raw.decode("ascii", errors="replace"))
1e2 82 ac 20 61
2€ a
3€ a
4��� a

The euro sign takes three bytes in UTF-8. Decoded as cp1252, a common Windows encoding, those three bytes become three wrong characters. Decoded as ASCII, they become replacement marks. Detecting the encoding before you decode keeps those characters intact.

Three ways text arrives broken: a PDF whose glyph positions scramble the reading order, OCR reading rn in modern as m to give modem, and the bytes E2 82 AC 20 61 decoding to the euro sign and a with the right rule and to replacement marks with the wrong one

Documenting the dataset

A dataset used without knowing where it came from leads to failures that are hard to debug. Nobody can judge whether a corpus fits their task without knowing how it was built. Three kinds of documentation answer that.

Datasheets for datasets

A datasheet is a standard set of questions that travels with a dataset and answers what a future user would otherwise have to guess. The answers fall into two groups:

For the support-ticket dataset, the datasheet would say that the company's own team built it to route tickets. It would also say that it holds messages from three months on every support channel, with names and emails removed before release.

A datasheet with three sections: provenance, who built it, who paid and why, process, how examples were selected and consent and preprocessing, and use, what it is suitable for and its known gaps

The goal is that someone can decide whether the dataset fits their task by reading it, without reverse-engineering the corpus.

Data statements and model cards

A data statement is documentation written for a text corpus in particular, and it focuses on the people behind the text:

A model card documents the model trained on the data instead. It says what the model was trained on and what it is meant for. It also shows how its performance breaks down across different groups of users or inputs, rather than one headline number.

A data statement describing the corpus with people, annotators, legal, collection and known gaps, next to a model card describing the model with training data, intended use, performance by group and limitations

Dataset documentation and model documentation answer different questions. Both let the next person reuse the work without guessing what happened.

Common mistakes

Labeling is the step this post skipped. It covers labeling guidelines for hard cases, crowd workers versus domain experts, and Cohen's kappa for measuring whether two labelers agree. Before any of that, it is worth checking the public catalogs on the Hugging Face Hub, Kaggle and academic repositories. After the dataset is built, text preprocessing reshapes the raw text into a consistent form for training.

QuiddityML teaches selecting data, extracting text and documenting datasets as separate concepts in the NLP track's data unit, with exercises such as picking the dataset that matches a support-email classifier's target population and saying what goes wrong when a scrape keeps the boilerplate (QuiddityML).