
Ai Cleaning Data
- 2 installs
- 11 repo stars
- Updated June 28, 2026
- lebsral/dspy-programming-not-prompting-lms-skills
Uses DSPy to normalize and fix messy data fields (addresses, phone numbers, dates, company names, free text) at scale to a defined target format.
About
Guides building a DSPy-based cleaner that takes a messy field value plus its type and returns a cleaned value with confidence. A developer uses it to standardize inconsistent data before import or analysis without hand-coding a rule per edge case.
- Sample anomalies, infer normalization rules, then apply deterministically where possible
- Single-field cleaner signature with explicit target format and confidence output
Ai Cleaning Data by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,759 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lebsral/dspy-programming-not-prompting-lms-skills --skill ai-cleaning-dataAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 11 |
| Last updated | June 28, 2026 |
| Repository | lebsral/dspy-programming-not-prompting-lms-skills ↗ |
What it does
Uses DSPy to normalize and fix messy data fields (addresses, phone numbers, dates, company names, free text) at scale to a defined target format.
Files
ai-cleaning-data
Use DSPy to normalize and fix messy data fields at scale. The core pattern - messy field value + field type/context → cleaned value + confidence - lets you handle inconsistent addresses, company names, dates, phone numbers, and free-text fields without writing a rule for every edge case.
The most effective approach: sample anomalies first, infer normalization rules, then apply deterministically where possible and use the LM only for ambiguous cases.
Step 1 - Understand the Cleaning Task
Before writing code, clarify:
- What fields need cleaning? (addresses, phone numbers, dates, company names, free-text?)
- What inconsistencies exist? (typos, format variations, abbreviations, mixed languages?)
- What is the target format? Always define this explicitly — otherwise the LM improvises
- How many rows? This determines whether to use LM for each row or rule inference + deterministic apply
- Is there a gold standard? Even 50 manually-cleaned examples make optimization possible
Step 2 - Build a Single-Field Cleaner
Start with one field type. The signature takes the messy value plus explicit format instructions.
import dspy
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
class CleanField(dspy.Signature):
"""Clean a messy data field to match the target format exactly.
Do not change values that are already correct.
Do not add, remove, or infer information not present in the input.
"""
messy_value: str = dspy.InputField(desc="The raw field value to clean")
field_type: str = dspy.InputField(desc="Type of field, e.g. 'US phone number', 'company name', 'ISO 8601 date'")
target_format: str = dspy.InputField(desc="Exact target format with example, e.g. '+1 (555) 123-4567'")
cleaned_value: str = dspy.OutputField(desc="The cleaned value in the target format, or the original if already correct")
confidence: float = dspy.OutputField(desc="Confidence score 0.0-1.0 that the cleaned value is correct")
change_made: bool = dspy.OutputField(desc="True if the value was changed, False if it was already correct")
cleaner = dspy.Predict(CleanField)
result = cleaner(
messy_value="(555)123-4567",
field_type="US phone number",
target_format="+1 (555) 123-4567"
)
print(result.cleaned_value) # "+1 (555) 123-4567"
print(result.confidence) # 0.97Step 3 - Common Cleaning Patterns
Address Normalization
class NormalizeAddress(dspy.Signature):
"""Normalize a US mailing address to USPS standard format.
Expand abbreviations (St → Street, Ave → Avenue, Apt → Apartment).
Capitalize properly. Do not infer or add missing components.
Preserve all components including suite/unit numbers.
"""
raw_address: str = dspy.InputField(desc="Raw address string")
city_hint: str = dspy.InputField(desc="City context if known, or empty string")
state_hint: str = dspy.InputField(desc="State context if known, or empty string")
normalized: str = dspy.OutputField(desc="USPS-format address: '123 Main Street, Suite 100, Springfield, IL 62701'")
confidence: float = dspy.OutputField(desc="Confidence 0.0-1.0")
address_cleaner = dspy.Predict(NormalizeAddress)Company Name Standardization
class StandardizeCompany(dspy.Signature):
"""Resolve a company name variant to its canonical legal name.
Examples - 'IBM Corp.' → 'IBM', 'I.B.M.' → 'IBM', 'Mickey D' → 'McDonald's'.
Use the canonical_name field for the authoritative form.
If the variant is unrecognizable, return it unchanged.
"""
variant: str = dspy.InputField(desc="Company name variant to standardize")
canonical_name: str = dspy.OutputField(desc="Canonical company name")
confidence: float = dspy.OutputField(desc="Confidence 0.0-1.0")
is_recognized: bool = dspy.OutputField(desc="True if the company was confidently identified")
company_cleaner = dspy.Predict(StandardizeCompany)Date Format Conversion
class NormalizeDate(dspy.Signature):
"""Convert a date string to ISO 8601 format (YYYY-MM-DD).
Handle formats like '05/04/26', 'May 4th 2026', '4-May-26', '20260504'.
If the date is ambiguous (e.g. 01/02/03), flag it.
"""
raw_date: str = dspy.InputField(desc="Raw date string in any format")
iso_date: str = dspy.OutputField(desc="Date in YYYY-MM-DD format, or empty string if unparseable")
is_ambiguous: bool = dspy.OutputField(desc="True if the date could be interpreted multiple ways")
confidence: float = dspy.OutputField(desc="Confidence 0.0-1.0")
date_cleaner = dspy.Predict(NormalizeDate)Step 4 - Rule Inference Pipeline
For large datasets, use the LM to infer rules from a sample, then apply deterministically.
class InferNormalizationRules(dspy.Signature):
"""Analyze a sample of messy field values and infer the normalization rules needed.
Output rules as a Python-executable list of (pattern, replacement) pairs where possible.
Identify which cases require LM judgment vs. deterministic transformation.
"""
field_type: str = dspy.InputField(desc="Type of field being analyzed")
target_format: str = dspy.InputField(desc="Target format with example")
sample_values: list[str] = dspy.InputField(desc="20-50 sample messy values")
deterministic_rules: list[str] = dspy.OutputField(desc="Rules expressible as regex/replace, one per line")
ambiguous_patterns: list[str] = dspy.OutputField(desc="Patterns that need LM judgment, one per line")
rule_coverage_estimate: float = dspy.OutputField(desc="Estimated % of rows covered by deterministic rules")
import pandas as pd
import re
def build_cleaning_pipeline(df: pd.DataFrame, column: str, field_type: str, target_format: str):
# Sample anomalies (skip already-clean values)
sample = df[column].dropna().sample(min(50, len(df))).tolist()
rule_inferrer = dspy.Predict(InferNormalizationRules)
rules = rule_inferrer(
field_type=field_type,
target_format=target_format,
sample_values=sample
)
print(f"Deterministic rules cover ~{rules.rule_coverage_estimate:.0%} of rows")
print("Rules:", rules.deterministic_rules)
print("Needs LM:", rules.ambiguous_patterns)
return rulesStep 5 - Validated Outputs with Pydantic
Use typed outputs to catch format violations before they reach your database.
from pydantic import BaseModel, field_validator
import re
class CleanedPhone(BaseModel):
original: str
cleaned: str
confidence: float
@field_validator("cleaned")
@classmethod
def must_match_e164(cls, v):
if v and not re.match(r"^\+1 \(\d{3}\) \d{3}-\d{4}$", v):
raise ValueError(f"Phone '{v}' does not match target format +1 (NNN) NNN-NNNN")
return v
class CleanPhoneTyped(dspy.Signature):
"""Clean a US phone number to +1 (NNN) NNN-NNNN format."""
raw: str = dspy.InputField()
result: CleanedPhone = dspy.OutputField()
phone_cleaner = dspy.TypedPredictor(CleanPhoneTyped)Step 6 - Batch Processing with Confidence Routing
Route high-confidence results to auto-accept and low-confidence ones to a human review queue.
def clean_batch(
values: list[str],
field_type: str,
target_format: str,
auto_accept_threshold: float = 0.90,
flag_threshold: float = 0.70,
) -> dict:
cleaner = dspy.Predict(CleanField)
accepted, flagged, rejected = [], [], []
for val in values:
result = cleaner(
messy_value=val,
field_type=field_type,
target_format=target_format
)
entry = {"original": val, "cleaned": result.cleaned_value, "confidence": result.confidence}
if result.confidence >= auto_accept_threshold:
accepted.append(entry)
elif result.confidence >= flag_threshold:
flagged.append(entry) # send to human review
else:
rejected.append(entry) # too uncertain, keep original or escalate
return {"accepted": accepted, "flagged": flagged, "rejected": rejected}Step 7 - Evaluate and Optimize
If you have a gold standard (even 50 rows), use it to optimize prompts.
# Build a gold standard dataset
trainset = [
dspy.Example(
messy_value="(555)123-4567",
field_type="US phone number",
target_format="+1 (555) 123-4567",
cleaned_value="+1 (555) 123-4567"
).with_inputs("messy_value", "field_type", "target_format"),
# ... more examples
]
def exact_match_metric(example, prediction, trace=None):
return example.cleaned_value == prediction.cleaned_value
from dspy.teleprompt import BootstrapFewShot
optimizer = BootstrapFewShot(metric=exact_match_metric, max_bootstrapped_demos=4)
optimized_cleaner = optimizer.compile(dspy.Predict(CleanField), trainset=trainset)When NOT to Use AI Cleaning
Use regex, pandas, or deterministic transforms instead when:
- Structured patterns cover 95%+ of cases - phone number regex, pandas
pd.to_datetime, stripping whitespace - Simple type coercion -
int(),float(),strip(),lower() - Already-clean data with a few outliers - just filter or drop the outliers
- You need 100% reproducibility - LM outputs are non-deterministic; use deterministic rules when the format is fully specified
- Cost matters at extreme scale - 10M rows × LM call is expensive; infer rules on a 1K sample and apply them
Key Patterns
| Task | DSPy approach |
|---|---|
| Single field, ad hoc | dspy.Predict(CleanField) |
| Validated output format | dspy.TypedPredictor with Pydantic |
| Iterative refinement on failures | dspy.Refine with format-check reward |
| Optimize on gold standard | BootstrapFewShot with exact-match metric |
| Rule inference at scale | Sample anomalies → infer rules → apply deterministically |
Gotchas
Calling the LM on every row instead of inferring rules first. For 10K+ rows, sample 20-50 anomalous values, ask the LM to infer normalization patterns, then apply them with pandas/regex. Reserve LM calls for the ambiguous remainder.
Not specifying the target format explicitly. If you write "clean the phone number" without showing the exact target format (e.g., +1 (555) 123-4567), Claude will pick a format. Always include a concrete example in target_format.
Using `dspy.Assert`/`dspy.Suggest` for format validation. These are deprecated. Use dspy.Refine with a reward function that checks the cleaned value against your format regex:
def format_reward(result, target_format_regex):
return 1.0 if re.match(target_format_regex, result.cleaned_value) else 0.0
cleaner = dspy.Refine(dspy.Predict(CleanField), N=3, reward_fn=format_reward)Cleaning related fields independently. Address components (street, city, state, zip) must be normalized together — passing only the street loses context needed to expand abbreviations correctly. Pass all related fields in a single signature.
Destructive normalization. Claude may silently drop components it considers "noise" (e.g., "Suite 100", "c/o Jane Smith", legal suffixes like "LLC"). Add a meaning_preserved output field and reject or flag any cleaned value where it is False.
Cross-References
Install any skill: npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill <name>/ai-parsing-data- extract structured fields from unstructured text (complement to cleaning)/ai-checking-outputs- validate cleaned values against schemas or business rules/dspy-refine- iterative refinement with a reward function, for format-check loops/dspy-modules- understand Predict, TypedPredictor, and other DSPy primitives/ai-generating-data- generate synthetic dirty data to build eval sets- Install `/ai-do` if you do not have it — it routes any AI problem to the right skill and is the fastest way to work:
npx skills add lebsral/DSPy-Programming-not-prompting-LMs-skills --skill ai-do
See examples.md for full worked examples - address normalizer, company name standardizer, and CSV batch cleaner.
last_audit:
date: 2026-05-04
score: 0/0
versions:
dspy: 3.2.0
[
{
"id": "phone-normalization",
"description": "Normalize US phone numbers in various formats to E.164-style +1 (NNN) NNN-NNNN",
"input": {
"values": [
"(415)555-1234",
"415.555.5678",
"+14155559012",
"415 555 3456",
"4155550000",
"1-415-555-7777"
],
"field_type": "US phone number",
"target_format": "+1 (415) 555-1234"
},
"expected": {
"cleaned_values": [
"+1 (415) 555-1234",
"+1 (415) 555-5678",
"+1 (415) 555-9012",
"+1 (415) 555-3456",
"+1 (415) 555-0000",
"+1 (415) 555-7777"
],
"all_high_confidence": true,
"min_confidence": 0.90
},
"metric": "exact_match_all",
"notes": "All inputs have deterministic resolutions. Regex should handle all 6 without LM calls."
},
{
"id": "company-name-resolution",
"description": "Resolve company name variants to canonical brand names",
"input": {
"variants": [
"IBM Corp.",
"I.B.M.",
"International Business Machines",
"MSFT",
"Amazn",
"Google LLC",
"Alphabet Inc.",
"XYZ Unknown Partners LLC"
]
},
"expected": {
"resolved": {
"IBM Corp.": "IBM",
"I.B.M.": "IBM",
"International Business Machines": "IBM",
"MSFT": "Microsoft",
"Amazn": "Amazon",
"Google LLC": "Google",
"Alphabet Inc.": "Alphabet"
},
"unrecognized": ["XYZ Unknown Partners LLC"],
"unrecognized_returned_unchanged": true
},
"metric": "exact_match_recognized_variants",
"notes": "Unknown companies must be returned unchanged, not hallucinated to a known company."
},
{
"id": "date-format-normalization",
"description": "Convert mixed date formats to ISO 8601 YYYY-MM-DD",
"input": {
"raw_dates": [
"05/04/2026",
"May 4th 2026",
"4-May-26",
"20260504",
"2026/05/04",
"May 4, 2026"
],
"ambiguous_dates": [
"01/02/03"
]
},
"expected": {
"iso_dates": {
"05/04/2026": "2026-05-04",
"May 4th 2026": "2026-05-04",
"4-May-26": "2026-05-04",
"20260504": "2026-05-04",
"2026/05/04": "2026-05-04",
"May 4, 2026": "2026-05-04"
},
"ambiguous_flagged": {
"01/02/03": true
},
"min_confidence": 0.88
},
"metric": "exact_match_unambiguous_dates",
"notes": "Ambiguous dates like 01/02/03 must be flagged with is_ambiguous=True rather than silently picking an interpretation."
}
]
ai-cleaning-data - Examples
Example 1 - Address Normalizer
Normalize messy US addresses to USPS standard format. Handles abbreviations, missing components, and inconsistent capitalization.
import dspy
import pandas as pd
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
class NormalizeAddress(dspy.Signature):
"""Normalize a US mailing address to USPS standard format.
Expand abbreviations - St → Street, Ave → Avenue, Blvd → Boulevard,
Apt → Apartment, Ste → Suite, Dr → Drive, Rd → Road.
Capitalize each word in street name, city, and state abbreviation.
Preserve all components including suite/unit/apartment numbers.
Do not infer or add information not present in the input.
If a component is missing, leave it absent - do not guess.
"""
raw_address: str = dspy.InputField(desc="Raw address string, may include city/state/zip")
normalized: str = dspy.OutputField(
desc="USPS-format address, e.g. '123 Main Street, Suite 100, Springfield, IL 62701'"
)
confidence: float = dspy.OutputField(desc="Confidence 0.0-1.0 that normalization is correct")
meaning_preserved: bool = dspy.OutputField(
desc="True if all components from input are present in output, False if any were dropped"
)
normalizer = dspy.Predict(NormalizeAddress)
test_addresses = [
"123 main st ste 100 springfield il 62701",
"456 Oak Ave., Apt 2B, Chicago, IL 60601",
"789 elm blvd, boston ma 02101",
"1000 W. Broadway Rd, Phoenix AZ, 85001",
"55 Park ave new york ny 10022",
]
results = []
for addr in test_addresses:
r = normalizer(raw_address=addr)
results.append({
"original": addr,
"normalized": r.normalized,
"confidence": r.confidence,
"meaning_preserved": r.meaning_preserved,
})
df = pd.DataFrame(results)
# Flag anything that lost meaning or is low confidence
flagged = df[(df["confidence"] < 0.85) | (~df["meaning_preserved"])]
print(f"Auto-accepted: {len(df) - len(flagged)}")
print(f"Flagged for review: {len(flagged)}")
print(df[["original", "normalized", "confidence"]].to_string())Sample output:
Auto-accepted: 4
Flagged for review: 1
original normalized confidence
123 main st ste 100 springfield il 62701 123 Main Street, Suite 100, Springfield... 0.97
456 Oak Ave., Apt 2B, Chicago, IL 60601 456 Oak Avenue, Apartment 2B, Chicago, ... 0.99
789 elm blvd, boston ma 02101 789 Elm Boulevard, Boston, MA 02101 0.94
1000 W. Broadway Rd, Phoenix AZ, 85001 1000 West Broadway Road, Phoenix, AZ 85001 0.91
55 Park ave new york ny 10022 55 Park Avenue, New York, NY 10022 0.88---
Example 2 - Company Name Standardizer
Resolve variant company names to their canonical forms. Useful before joining datasets or deduplicating CRM records.
import dspy
from pydantic import BaseModel
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
class CompanyResolution(BaseModel):
canonical_name: str
confidence: float
is_recognized: bool
variant_type: str # e.g. "abbreviation", "common name", "legal suffix", "typo", "already canonical"
class StandardizeCompany(dspy.Signature):
"""Resolve a company name variant to its well-known canonical name.
Examples of resolutions:
- 'IBM Corp.' → 'IBM' (legal suffix removal)
- 'I.B.M.' → 'IBM' (abbreviation expansion)
- 'International Business Machines' → 'IBM' (full legal name → brand)
- 'Mickey D' → 'McDonald's' (common nickname)
- 'Alphabet Inc.' → 'Alphabet' (legal suffix)
- 'MSFT' → 'Microsoft' (stock ticker)
- 'Amazn' → 'Amazon' (typo)
If the variant cannot be confidently resolved, return it unchanged with is_recognized=False.
"""
variant: str = dspy.InputField(desc="Company name variant to standardize")
result: CompanyResolution = dspy.OutputField(desc="Resolution result with canonical name and metadata")
standardizer = dspy.TypedPredictor(StandardizeCompany)
variants = [
"IBM Corp.",
"I.B.M.",
"International Business Machines Corporation",
"MSFT",
"Amazn", # typo
"Mickey D's",
"Alphabet Inc.",
"Google LLC",
"McKinsey & Company",
"Accenture PLC",
"XYZ Consulting Partners", # unknown
]
print(f"{'Variant':<40} {'Canonical':<30} {'Type':<20} {'Conf'}")
print("-" * 100)
for v in variants:
r = standardizer(variant=v).result
flag = "" if r.is_recognized else " [UNKNOWN]"
print(f"{v:<40} {r.canonical_name:<30} {r.variant_type:<20} {r.confidence:.2f}{flag}")Sample output:
Variant Canonical Type Conf
----------------------------------------------------------------------------------------------------
IBM Corp. IBM legal suffix 0.99
I.B.M. IBM abbreviation 0.98
International Business Machines Corpo... IBM full legal name 0.97
MSFT Microsoft stock ticker 0.99
Amazn Amazon typo 0.92
Mickey D's McDonald's common nickname 0.95
Alphabet Inc. Alphabet legal suffix 0.98
Google LLC Google legal suffix 0.99
McKinsey & Company McKinsey & Company already canonical 0.96
Accenture PLC Accenture legal suffix 0.97
XYZ Consulting Partners XYZ Consulting Partners already canonical 0.41 [UNKNOWN]---
Example 3 - CSV Batch Cleaner
Clean a CSV with mixed date formats, inconsistent phone numbers, and typos in a category column. Uses rule inference to minimize LM calls.
import dspy
import pandas as pd
import re
from io import StringIO
lm = dspy.LM("openai/gpt-4o-mini") # or "anthropic/claude-sonnet-4-5-20250929", etc.
dspy.configure(lm=lm)
# Simulated messy CSV
MESSY_CSV = """id,name,phone,signup_date,category
1,Alice Chen,(415)555-1234,05/04/2026,enterprise
2,Bob Smith,415.555.5678,2026-05-04,Enterprise
3,Carol Wu,+14155559012,May 4th 2026,SMB
4,Dave Lee,415 555 3456,4-May-26,smb
5,Eve Park,4155550000,20260504,Mid-market
6,Frank Kim,(415) 555-1111,05/04/26,mid market
7,Grace Ho,415-555-2222,2026/05/04,ENTERPRISE
8,Hiro Ito,555-3333,May 2026,Unknown
"""
df = pd.read_csv(StringIO(MESSY_CSV))
print("Original data:")
print(df.to_string(index=False))
print()
# --- Clean phone numbers ---
# Try regex first for the most common patterns
def clean_phone_regex(raw: str) -> str | None:
"""Handle patterns we can resolve deterministically."""
digits = re.sub(r"\D", "", str(raw))
if len(digits) == 10:
return f"+1 ({digits[:3]}) {digits[3:6]}-{digits[6:]}"
elif len(digits) == 11 and digits[0] == "1":
return f"+1 ({digits[1:4]}) {digits[4:7]}-{digits[7:]}"
return None # needs LM
class CleanPhone(dspy.Signature):
"""Clean an ambiguous US phone number to +1 (NNN) NNN-NNNN format.
Only use this for numbers that could not be parsed by regex.
"""
raw_phone: str = dspy.InputField()
cleaned: str = dspy.OutputField(desc="Phone in +1 (NNN) NNN-NNNN format, or empty string if not a valid US number")
confidence: float = dspy.OutputField()
phone_cleaner = dspy.Predict(CleanPhone)
cleaned_phones = []
lm_calls = 0
for raw in df["phone"]:
result = clean_phone_regex(str(raw))
if result is None:
r = phone_cleaner(raw_phone=str(raw))
result = r.cleaned
lm_calls += 1
cleaned_phones.append(result)
df["phone_clean"] = cleaned_phones
print(f"Phone cleaning - {lm_calls} LM calls out of {len(df)} rows (regex handled the rest)")
# --- Clean dates ---
class CleanDate(dspy.Signature):
"""Convert any date string to ISO 8601 YYYY-MM-DD format."""
raw_date: str = dspy.InputField()
iso_date: str = dspy.OutputField(desc="Date as YYYY-MM-DD, or empty string if unparseable")
is_ambiguous: bool = dspy.OutputField(desc="True if the date could be multiple interpretations")
confidence: float = dspy.OutputField()
date_cleaner = dspy.Predict(CleanDate)
# Try pandas first (handles many ISO variants)
def clean_date_pandas(raw: str):
try:
return pd.to_datetime(raw).strftime("%Y-%m-%d"), False
except Exception:
return None, False
cleaned_dates, ambiguous_flags = [], []
lm_date_calls = 0
for raw in df["signup_date"]:
iso, ambig = clean_date_pandas(str(raw))
if iso is None:
r = date_cleaner(raw_date=str(raw))
iso = r.iso_date
ambig = r.is_ambiguous
lm_date_calls += 1
cleaned_dates.append(iso)
ambiguous_flags.append(ambig)
df["date_clean"] = cleaned_dates
df["date_ambiguous"] = ambiguous_flags
print(f"Date cleaning - {lm_date_calls} LM calls out of {len(df)} rows")
# --- Clean categories ---
# Normalize to canonical set: Enterprise, SMB, Mid-Market
CATEGORY_MAP = {
"enterprise": "Enterprise",
"smb": "SMB",
"mid-market": "Mid-Market",
"mid market": "Mid-Market",
}
def clean_category(raw: str) -> str | None:
normalized = raw.strip().lower()
return CATEGORY_MAP.get(normalized)
class CleanCategory(dspy.Signature):
"""Map a company category variant to one of: Enterprise, SMB, Mid-Market.
Return the original value if it does not match any of these categories.
"""
raw_category: str = dspy.InputField()
canonical: str = dspy.OutputField(desc="One of: Enterprise, SMB, Mid-Market, or the original if unrecognized")
confidence: float = dspy.OutputField()
cat_cleaner = dspy.Predict(CleanCategory)
cleaned_cats = []
lm_cat_calls = 0
for raw in df["category"]:
result = clean_category(str(raw))
if result is None:
r = cat_cleaner(raw_category=str(raw))
result = r.canonical
lm_cat_calls += 1
cleaned_cats.append(result)
df["category_clean"] = cleaned_cats
print(f"Category cleaning - {lm_cat_calls} LM calls out of {len(df)} rows")
# --- Summary ---
print("\nCleaned data:")
print(df[["id", "name", "phone_clean", "date_clean", "category_clean", "date_ambiguous"]].to_string(index=False))
flagged = df[df["date_ambiguous"]]
if not flagged.empty:
print(f"\nFlagged for review (ambiguous dates): {len(flagged)} rows")
print(flagged[["id", "name", "signup_date", "date_clean"]].to_string(index=False))Sample output:
Phone cleaning - 1 LM calls out of 8 rows (regex handled the rest)
Date cleaning - 3 LM calls out of 8 rows
Category cleaning - 1 LM calls out of 8 rows
Cleaned data:
id name phone_clean date_clean category_clean date_ambiguous
1 Alice Chen +1 (415) 555-1234 2026-05-04 Enterprise False
2 Bob Smith +1 (415) 555-5678 2026-05-04 Enterprise False
3 Carol Wu +1 (415) 555-9012 2026-05-04 SMB False
4 Dave Lee +1 (415) 555-3456 2026-05-04 SMB False
5 Eve Park +1 (415) 555-0000 2026-05-04 Mid-Market False
6 Frank Kim +1 (415) 555-1111 2026-05-04 Mid-Market False
7 Grace Ho +1 (415) 555-2222 2026-05-04 Enterprise False
8 Hiro Ito +1 () 2026-05-01 [UNKNOWN] True
Flagged for review (ambiguous dates): 1 rows
id name signup_date date_clean
8 Hiro Ito May 2026 2026-05-01Key takeaway: By applying regex and pandas before the LM, only 5 out of 24 field-level operations required an LM call — an 80% cost reduction on this dataset.