Pipeline to Dashboardnihalsulaiman.com

Week 2 · Clean and trust the data › Day 11

Day 11. Cleaning with Claude Code and open models

⏱ 4h · learn 70m · lab 130m · ship 25m · review 15m🏷 retail (supplier PDFs), QSRBefore: Day 10

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

  1. Context first. A CLAUDE.md in 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).
  2. Ask for a plan, not code. "Read docs/profiles/qsr_pos.md and clean/rules.py. Propose how to handle duplicated POS batches. Don't write code yet."
  3. Tests before implementation. "Write failing tests for the duplicate rule using the examples in the profile."
  4. Implement until green, then ask it to explain each line you don't understand.
  5. Evidence. Row counts before/after every step, saved with the query that produced them.
  6. 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

  1. Write CLAUDE.md for qsr-clean with the rules above. Run the 6-step loop to implement duplicate POS batch removal. Save your prompts in docs/ai-log.md with what you accepted and rejected.
  2. Make 10 fake supplier invoices: ask Claude Code to write make_invoices.py that renders random invoices to PDF with reportlab (varying layouts). Keep the true values in a JSON answer key.
  3. Install Ollama, pull a small model (ollama pull qwen2.5:7b or a smaller one if your machine struggles), and extract all 10 with the code above.
  4. Score against the answer key: field accuracy, documents passing the total check. Put the table in the README.
  5. 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?

2. An LLM-extracted invoice's lines don't add up to its total. Best action?

3. When is a local open model the better choice than a hosted frontier model?

My notes