This guide walks you through setting up and running a complete data labeling workflow β from raw unlabeled data to model-ready training labels.
βββββββββββββββ ββββββββββββββββ βββββββββββββββββ ββββββββββββ
β Raw Data β β β Task Queue β β β Annotation β β β Consensusβ
β (unlabeled) β β (prioritised)β β (N annotators)β β Resolutionβ
βββββββββββββββ ββββββββββββββββ βββββββββββββββββ ββββββββββββ
β
βββββββββββββββ ββββββββββββββββ βββββββββββββββββ β
β Training β β β Export β β β Quality Audit β βββββββββ
β Dataset β β (approved) β β & Adjudicationβ
βββββββββββββββ ββββββββββββββββ βββββββββββββββββ
Before anything else, decide what labels you need. Open configs/schema_config.yaml and define:
from src.schema import LabelSchema, Category, LabelType
schema = LabelSchema(
name="my_project",
version="1.0",
label_type=LabelType.CLASSIFICATION,
categories=[
Category(
name="positive",
description="Expresses satisfaction or approval",
examples=["Great product!", "Highly recommend"],
counter_examples=["It's okay (β neutral)"],
),
# ... more categories
],
)Key decisions:
Edit configs/labeling_config.yaml. The critical settings:
| Setting | Recommendation | Why |
|---|---|---|
redundancy | 3 for classification, 5 for subjective | More raters = better agreement measurement |
assignment_strategy | stratified | Routes hard items to experienced annotators |
consensus.min_agreement | 0.67 (2/3) | Balances quality vs. throughput |
quality.golden_items.fraction | 0.10 | Catches random clicking without excessive cost |
active_learning.strategy | entropy | Best general-purpose uncertainty measure |
Create Task objects from your unlabeled data:
from src.workflow import Task, WorkflowEngine
tasks = [
Task(
task_id=f"item_{i:04d}",
data={"text": row["text"]},
difficulty="normal", # or "easy" / "hard"
)
for i, row in enumerate(your_data)
]
engine = WorkflowEngine(redundancy=3, min_agreement=0.67)
engine.add_tasks(tasks)annotator_ids = ["ann_alice", "ann_bob", "ann_charlie"]
batch = engine.create_batch(
batch_id="batch_001",
annotator_ids=annotator_ids,
strategy="round_robin",
max_tasks=50,
)As annotators work (through your UI or the included templates/labeling_interface.html):
engine.submit_label(
assignment_id="asgn_abc123",
label="positive",
duration_sec=4.2,
)Once all annotators have completed their assignments:
results = engine.resolve_all()
for result in results:
if result.needs_adjudication:
print(f" {result.task_id} needs expert review")
else:
print(f" {result.task_id} β '{result.final_label}'")Tasks that can't reach consensus go to a senior annotator:
for task in engine.tasks_needing_adjudication:
# Show the task + all submitted labels to the expert
engine.adjudicate(task.task_id, "neutral", adjudicator_id="expert_01")from src.quality_audit import generate_audit_report, Annotation
# Convert engine's internal data to Annotation objects for auditing
annotations = [...] # build from engine._assignments
report = generate_audit_report(annotations)
print(report)approved_data = engine.export_approved()
# β [{"task_id": ..., "data": {...}, "label": ..., "metadata": {...}}, ...]For maximum labeling efficiency, use the active learning module:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β ββββββββββββ βββββββββββββ ββββββββββ β
βββ β Train β β β Score β β β Select β ββ
β Model β β Unlabeled β β Top-K β
ββββββββββββ βββββββββββββ ββββββββββ
β
β
ββββββββββββ
β Label β
β Selected β
ββββββββββββ
from src.active_learning import entropy_sampling
# After training your model on the current labeled set:
probs = model.predict_proba(X_unlabeled)
result = entropy_sampling(probs.tolist(), budget=100)
# Queue the selected samples for labeling
selected_tasks = [all_tasks[i] for i in result.indices]
engine.add_tasks(selected_tasks)When you need to add/rename/merge categories mid-project:
from src.schema import SchemaMigration, MigrationStep, MigrationAction
migration = SchemaMigration(
from_version="1.0",
to_version="2.0",
steps=[
MigrationStep(
action=MigrationAction.ADD_CATEGORY,
params={"name": "neutral"},
reason="Annotators struggled with ambiguous reviews",
),
],
)
# Apply to existing labels
migrated_labels, n_changed = migration.apply_to_labels(existing_labels)| Problem | Symptom | Solution |
|---|---|---|
| Low agreement | ΞΊ < 0.4 | Rewrite category descriptions with more examples |
| Annotator fatigue | Speed increases, accuracy drops per batch | Limit batch size to 50 items |
| Class imbalance | One label dominates > 80% | Use stratified sampling for batches |
| Schema creep | Too many categories | Merge rare categories, split only when needed |
| Gold item bias | Annotators recognise gold items | Rotate gold set regularly |
1. Inter-annotator agreement (Cohen's ΞΊ / Fleiss' ΞΊ): target β₯ 0.6
2. Adjudication rate: should decrease over time as guidelines improve
3. Label velocity: items labeled per annotator per hour
4. Active learning efficiency: accuracy gain per labeled sample vs. random
5. Distribution drift: chi-squared p-value between weekly batches
Part of ML Engineer Toolkit
Get the full Data Labeling Pipeline and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.