Week 2 · Clean and trust the data › Day 11
Day 11. Cleaning with Claude Code and open models
By the end of today you can
- Use Claude Code to profile, write and test cleaning code without losing control
- Write a project CLAUDE.md and a reusable skill for cleaning tasks
- Extract structured data from PDFs and free text with an LLM and a JSON schema
- Run a local open model with Ollama and judge when it's good enough
1. Why this matters
You saw AI-assisted cleaning on a real team. Done well, it turns a day of parsing work into an hour. Done badly, it produces plausible code that quietly drops rows. The difference is a workflow: the AI proposes, tests prove, you decide.
2. A safe Claude Code loop for data work
- Context first. A
CLAUDE.mdin the repo: what the project is, where raw data lives, the hard rules (raw is read-only, never print secrets, every rule gets a test, propose before changing shared things). - Ask for a plan, not code. "Read
docs/profiles/qsr_pos.mdandclean/rules.py. Propose how to handle duplicated POS batches. Don't write code yet." - Tests before implementation. "Write failing tests for the duplicate rule using the examples in the profile."
- Implement until green, then ask it to explain each line you don't understand.
- Evidence. Row counts before/after every step, saved with the query that produced them.
- Review the diff yourself. Reject anything that changes a test to make it pass.
<!-- CLAUDE.md (excerpt) -->
## Rules
- data/raw/ is read-only. Never write, move or delete there.
- Every cleaning rule lives in clean/rules.py with a docstring (source, why, date confirmed) and a test.
- Report row counts in/out for every transform. Dropped rows go to data/rejects/ with a reason column.
- Don't print values from columns named *email*, *phone*, *card*. Mask them in output.
- Propose schema changes; don't apply them.
Turn a repeated task into a skill
Your bi-skills-kit has the pattern. A clean-new-source skill can encode the profiling checklist from Day 8, the severity table and the test-first loop, so every new source gets the same treatment. Keep skills short and procedural.
3. LLMs for unstructured data
Some sources aren't tables: supplier invoices as PDFs, guest reviews, free-text delivery notes. An LLM can turn them into rows when you give it a schema and validate the output.
from pydantic import BaseModel, Field
from typing import Literal
class InvoiceLine(BaseModel):
sku: str | None
description: str
qty: float = Field(gt=0)
unit_price: float = Field(ge=0)
class Invoice(BaseModel):
supplier: str
invoice_no: str
invoice_date: str = Field(description="ISO date YYYY-MM-DD")
currency: Literal["CAD", "USD"]
lines: list[InvoiceLine]
total: float
# Local, free: Ollama with structured output
import ollama
resp = ollama.chat(model="qwen2.5:7b", format=Invoice.model_json_schema(),
messages=[{"role": "user", "content": f"Extract this invoice as JSON.\n\n{pdf_text}"}],
options={"temperature": 0})
inv = Invoice.model_validate_json(resp.message.content)
assert abs(sum(l.qty * l.unit_price for l in inv.lines) - inv.total) < 0.05, "lines don't add to total"
The same code works with the Claude API (tool use / structured output) when accuracy matters more than cost. Get the PDF text with pdfplumber or pymupdf first; send page images only when layout matters.
Maths and principle
Principle: never trust a single extraction; trust a checked extraction. Internal consistency checks catch most errors for free.
Equation: accept iff \(\left|\sum_i q_i p_i - T\right| < \varepsilon\) and every field passes its schema. Measured accuracy on a labelled sample of \(n\) documents: \(\hat a = k/n\) with a 95% interval of about \(\hat a \pm 1.96\sqrt{\hat a(1-\hat a)/n}\).
Analogy: a conservation law: if mass in ≠ mass out, the measurement is wrong somewhere. Where it breaks: a model can misread both a line and the total consistently. Keep a human-reviewed sample.
Try it: 47 of 50 sampled invoices correct. Rough 95% interval? (0.94 ± 0.066, so about 87%–100%. Label more before promising "95%".)
4. Choosing the model
| Option | Cost | Privacy | Good for |
|---|---|---|---|
| Rules / regex | Free | Local | Fixed layouts. Always try first |
| Local open model (Ollama: Qwen, Llama, Mistral, Gemma families) | Free after hardware | Data never leaves | Bulk, simple schemas, private data |
| Hosted frontier model (e.g. Claude) | Per token | Check data agreements | Messy layouts, reasoning, low volume high value |
The 5-year lens
The professional move is a cascade: rules first; a small local model for what rules miss; a frontier model only for what fails validation; a human queue for what still fails. Log which tier handled each document and its cost. That table is how you justify the approach to a finance manager, and it's exactly the "open-source, low-cost" mindset of Week 7.
Watch out
Never paste customer personal data or credentials into a chat window. For real data, use an approved API with a data-processing agreement, or a local model. In this course everything is synthetic, so practise freely.
Lab
- Write
CLAUDE.mdforqsr-cleanwith the rules above. Run the 6-step loop to implement duplicate POS batch removal. Save your prompts indocs/ai-log.mdwith what you accepted and rejected. - Make 10 fake supplier invoices: ask Claude Code to write
make_invoices.pythat renders random invoices to PDF withreportlab(varying layouts). Keep the true values in a JSON answer key. - Install Ollama, pull a small model (
ollama pull qwen2.5:7bor a smaller one if your machine struggles), and extract all 10 with the code above. - Score against the answer key: field accuracy, documents passing the total check. Put the table in the README.
- Stretch: add a fallback tier that sends failures to the Claude API, and log cost per document.
Ship
feat(clean): LLM invoice extraction with schema validation and scored accuracy.
Check yourself
1. Claude Code changes a test so that its new code passes. What should you do?
Tests are the contract. Changing them to pass hides a bug.
2. An LLM-extracted invoice's lines don't add up to its total. Best action?
Validation failures escalate; they never pass silently.
3. When is a local open model the better choice than a hosted frontier model?
Match the tool to volume, difficulty and privacy.