Every warehouse has them: company_name with "IBM", "I.B.M.", and "International Business Machines"; country with "USA", "U.S.", and "United States of America"; free-text categories nobody ever validated. Regex and lookup tables fix the easy 70%.
Tier: Foundation · Workflow area: Unstructured data processing
This playbook uses an LLM to close the remaining gap — but with one non-negotiable rule: the model classifies into a closed reference list, it never invents values. Generated strings are how you end up with a 201st country.
Skip the LLM when a deterministic mapping table already covers the distinct values — an LLM call per row for a solved lookup is pure cost.
Profile before you prompt. Pull the distinct values and their frequencies. You standardize distinct values, not rows — a 50M-row table often has only 3,000 distinct company strings.
SELECT company_name_raw, COUNT(*) AS rows
FROM raw.customers
GROUP BY 1 ORDER BY 2 DESC;
Run the deterministic baseline first. Exact match, casefold, punctuation-strip, then fuzzy match against the reference list. Record the fix-rate — the LLM only sees survivors, and the baseline is the number the LLM must beat to justify its cost.
Define the schema with the reference list embedded as allowed values. Confidence and an explicit "no match" escape hatch are part of the contract:
from pydantic import BaseModel
from typing import Optional
class Standardized(BaseModel):
raw_value: str
canonical_value: Optional[str] # must be from the reference list, or None
confidence: float # 0.0 - 1.0
reasoning: str
Classify the residue in batches, as a loop over the API. Ground every request in the reference list so the model maps, never invents:
import anthropic
client = anthropic.Anthropic()
REFERENCE = load_reference_list() # canonical values, e.g. from the dim table
results = []
for value in unresolved_values:
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=1024,
thinking={"type": "adaptive"},
messages=[{
"role": "user",
"content": (
"Map this raw company name to ONE canonical value from the "
f"reference list below, or null if none applies.\n"
f"Raw value: {value}\n"
f"Reference list: {REFERENCE}\n"
"Never output a value that is not in the list."
),
}],
output_format=Standardized,
)
results.append(response.parsed_output)
Expected output per value: a Standardized object like {"raw_value": "I.B.M. Corp", "canonical_value": "IBM", "confidence": 0.97, ...}.
Apply the validation gate — schema, allowed values, confidence, review queue. The SDK already enforced the schema; you still enforce membership and confidence:
accepted, review_queue = [], []
for r in results:
if r.canonical_value is not None and r.canonical_value not in REFERENCE:
review_queue.append(r) # invented value — hard reject
elif r.confidence >= 0.85 and r.canonical_value:
accepted.append(r)
else:
review_queue.append(r) # low confidence or no match
Accepted rows land in a value_mapping table; the review queue goes to a human, and every human decision is appended to the mapping table so it never has to be asked again.
Backfill through the mapping table, not per-row calls. Join raw → value_mapping → canonical in SQL. The LLM ran once per distinct value; the 50M-row update is a plain join.
Sample-audit before trusting the fix-rate. Hand-check 100 random accepted mappings. If more than 2–3 are wrong, raise the confidence threshold and rerun the audit before backfilling.
The reference list is the guardrail. An LLM allowed to emit free text will hallucinate plausible canonical values; an LLM forced to pick from a closed list (with null as an option) can only be wrong in reviewable ways.
SELECT COUNT(*) of mapped values not present in the reference dimension must be zero.baseline, llm, human) keep the audit trail and stop churn.On client engagements, treat raw values as potentially sensitive: company and person names can be client-confidential even when the schema is not. Get written approval for which columns may be sent to an external API, and send only the distinct values, never surrounding row context. If no external API is approved, run the identical workflow against a locally hosted model via vLLM or Ollama — the reference-list prompt, validation gate, and audit sample are unchanged — or use the client's own warehouse-native AI functions (Cortex/BigQuery) so data never leaves their perimeter. Deliver the mapping table plus the audit report, and document the fix-rate versus the deterministic baseline so the client can judge the LLM step's ongoing value.