Spaces:
Running
Running
| """Real-world vocabularies + a corruption engine for hard, self-verifiable canonicalization. | |
| The toy pools (4 countries, 3 category sets) taught the model nothing the heuristic | |
| didn't already do. Here we back categorical columns with hundreds of REAL canonical | |
| entities (countries, US states, currencies, cities, departments, job titles, statuses) | |
| and corrupt each into realistic surface forms (aliases, codes, casing, punctuation, | |
| typos) — recording the exact surface→canonical mapping so the example still round-trips | |
| through scrubdata.executor (self-verified). | |
| Design rule that keeps it learnable AND verifiable: | |
| - Each generated column draws only a FEW canonicals (low cardinality) so every dirty | |
| surface appears in the profile sample the model sees. | |
| - No outer whitespace on categorical cells (executor's canonicalize strips only outer | |
| space and would otherwise leave canonical-valued cells untrimmed) — casing/punct/typo | |
| noise is what creates the mapping entries. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| import string | |
| import pycountry | |
| # --- generic surface corruption --------------------------------------------- | |
| _KEEP = set(string.ascii_letters + string.digits + " ") | |
| def _strip_punct(s: str) -> str: | |
| return "".join(ch for ch in s if ch in _KEEP).strip() | |
| def _one_typo(rng: random.Random, s: str) -> str: | |
| if len(s) < 4: | |
| return s | |
| i = rng.randrange(1, len(s) - 1) | |
| mode = rng.random() | |
| if mode < 0.28: # drop a char | |
| return s[:i] + s[i + 1:] | |
| if mode < 0.52: # swap adjacent | |
| return s[:i] + s[i + 1] + s[i] + s[i + 2:] | |
| if mode < 0.72: # duplicate a char | |
| return s[:i] + s[i] + s[i:] | |
| # substitute a char (the classic 'birminghxm' corruption) | |
| repl = rng.choice(string.ascii_uppercase if s[i].isupper() else string.ascii_lowercase) | |
| return s[:i] + repl + s[i + 1:] | |
| def make_surface(rng: random.Random, canonical: str, aliases: list[str], | |
| typo_p: float = 0.13) -> str: | |
| """Produce one realistic dirty surface for a canonical value. | |
| `typo_p` is the per-cell probability of injecting a single-char typo. The | |
| high-cardinality path raises this (column-level parameter) so a column reliably | |
| carries a long tail of single-char typos rather than a thin sprinkle. | |
| """ | |
| s = rng.choice([canonical, *aliases]) if aliases else canonical | |
| r = rng.random() | |
| if r < 0.28: | |
| s = s.lower() | |
| elif r < 0.42: | |
| s = s.upper() | |
| elif r < 0.52: | |
| s = s.title() | |
| if rng.random() < 0.15: | |
| s = _strip_punct(s) | |
| if rng.random() < typo_p: | |
| s = _one_typo(rng, s) | |
| return s.strip() | |
| def make_substitution_typo(rng: random.Random, canonical: str) -> str: | |
| """Guaranteed single-char SUBSTITUTION typo of the canonical (birminghxm regime). | |
| Returns the canonical unchanged only when too short to safely corrupt; callers | |
| that require a real typo should check `surface != canonical`. | |
| """ | |
| if len(canonical) < 4: | |
| return canonical | |
| i = rng.randrange(1, len(canonical) - 1) | |
| ch = canonical[i] | |
| if not ch.isalpha(): | |
| # find any interior alpha char to substitute, else give up | |
| alpha_idx = [j for j in range(1, len(canonical) - 1) if canonical[j].isalpha()] | |
| if not alpha_idx: | |
| return canonical | |
| i = rng.choice(alpha_idx) | |
| ch = canonical[i] | |
| pool = string.ascii_uppercase if ch.isupper() else string.ascii_lowercase | |
| repl = rng.choice([c for c in pool if c != ch.lower() and c != ch.upper()]) | |
| return canonical[:i] + repl + canonical[i + 1:] | |
| # --- vocabularies (canonical -> aliases) ------------------------------------ | |
| # Curated common aliases for high-frequency countries (codes are added automatically). | |
| _COUNTRY_ALIASES = { | |
| "United States": ["USA", "U.S.A.", "U.S.", "America", "the US", "United States of America"], | |
| "United Kingdom": ["UK", "U.K.", "Britain", "Great Britain", "England"], | |
| "United Arab Emirates": ["UAE", "U.A.E."], | |
| "South Korea": ["Korea", "Republic of Korea"], | |
| "Russian Federation": ["Russia"], | |
| "Czechia": ["Czech Republic"], | |
| "Netherlands": ["Holland", "The Netherlands"], | |
| } | |
| def _country_entries(limit: int | None = None) -> dict[str, list[str]]: | |
| out: dict[str, list[str]] = {} | |
| for c in pycountry.countries: | |
| name = c.name | |
| aliases = {c.alpha_2, c.alpha_3} | |
| official = getattr(c, "official_name", None) | |
| if official and official != name: | |
| aliases.add(official) | |
| aliases.update(_COUNTRY_ALIASES.get(name, [])) | |
| out[name] = sorted(a for a in aliases if a and a != name) | |
| if limit and len(out) >= limit: | |
| break | |
| return out | |
| # USPS-style: pycountry subdivision code is "US-CA"; the bare code "CA" is the alias. | |
| def _subdivision_entries(country_code: str = "US") -> dict[str, list[str]]: | |
| out: dict[str, list[str]] = {} | |
| for s in pycountry.subdivisions.get(country_code=country_code) or []: | |
| bare = s.code.split("-")[-1] | |
| out[s.name] = [bare] | |
| return out | |
| def _currency_entries(limit: int = 60) -> dict[str, list[str]]: | |
| # Canonical = ISO code (USD); aliases = the long name and a few symbols. | |
| symbols = {"USD": ["$", "US$"], "EUR": ["€"], "GBP": ["£"], "JPY": ["¥"], "INR": ["₹"]} | |
| out: dict[str, list[str]] = {} | |
| for c in pycountry.currencies: | |
| code = c.alpha_3 | |
| out[code] = [c.name, *symbols.get(code, [])] | |
| if len(out) >= limit: | |
| break | |
| return out | |
| _CITY_ALIASES = { | |
| "New York": ["NYC", "New York City", "NY"], "Los Angeles": ["LA", "L.A."], | |
| "San Francisco": ["SF", "San Fran"], "Las Vegas": ["Vegas"], | |
| "Washington": ["Washington D.C.", "DC", "Washington DC"], "Chicago": ["Chi-town"], | |
| "London": ["LDN"], "Mexico City": ["CDMX", "Ciudad de Mexico"], | |
| "Sao Paulo": ["São Paulo", "SP"], "Tokyo": ["Tōkyō"], "Mumbai": ["Bombay"], | |
| "Bengaluru": ["Bangalore"], "Istanbul": ["Constantinople"], "Beijing": ["Peking"], | |
| "Philadelphia": ["Philly"], "New Orleans": ["NOLA"], "Hong Kong": ["HK"], | |
| "Rio de Janeiro": ["Rio"], "Buenos Aires": ["BA"], "Amsterdam": ["A'dam"], | |
| } | |
| def _department_entries() -> dict[str, list[str]]: | |
| return { | |
| "Engineering": ["Eng", "Eng.", "R&D", "Dev"], "Sales": ["Biz Dev"], | |
| "Marketing": ["Mktg", "Mkt", "Growth"], "Human Resources": ["HR", "People", "People Ops"], | |
| "Finance": ["Fin", "Accounting"], "Operations": ["Ops"], | |
| "Customer Support": ["Support", "CS", "Cust Support"], "Legal": ["Legal & Compliance"], | |
| "Product": ["Prod", "PM"], "Information Technology": ["IT", "I.T."], | |
| } | |
| def _job_title_entries() -> dict[str, list[str]]: | |
| return { | |
| "Senior Engineer": ["Sr. Engineer", "Sr Engineer", "Senior Eng", "Snr Engineer"], | |
| "Engineering Manager": ["Eng Manager", "Eng Mgr", "Engineering Mgr"], | |
| "Chief Executive Officer": ["CEO", "C.E.O."], | |
| "Chief Technology Officer": ["CTO", "C.T.O."], | |
| "Vice President": ["VP", "V.P.", "Vice Pres"], | |
| "Account Executive": ["AE", "Acct Exec"], "Sales Representative": ["Sales Rep", "Rep"], | |
| "Product Manager": ["PM", "Prod Manager", "Prod Mgr"], | |
| "Administrative Assistant": ["Admin Assistant", "Admin Asst", "Admin"], | |
| "Director": ["Dir", "Dir."], | |
| } | |
| CITIES = [ | |
| "New York", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", | |
| "San Antonio", "San Diego", "Dallas", "San Francisco", "Austin", "Seattle", | |
| "Denver", "Boston", "Washington", "Las Vegas", "Portland", "Miami", "Atlanta", | |
| "New Orleans", "London", "Paris", "Berlin", "Madrid", "Rome", "Amsterdam", | |
| "Lisbon", "Dublin", "Vienna", "Prague", "Warsaw", "Stockholm", "Oslo", | |
| "Tokyo", "Osaka", "Seoul", "Beijing", "Shanghai", "Mumbai", "Delhi", | |
| "Bengaluru", "Singapore", "Hong Kong", "Bangkok", "Jakarta", "Manila", | |
| "Sydney", "Melbourne", "Toronto", "Vancouver", "Montreal", "Mexico City", | |
| "Sao Paulo", "Rio de Janeiro", "Buenos Aires", "Bogota", "Lima", "Santiago", | |
| "Cairo", "Lagos", "Nairobi", "Johannesburg", "Dubai", "Istanbul", | |
| ] | |
| # Lazily-built, cached vocab dicts (pycountry iteration is a touch slow). | |
| _CACHE: dict[str, dict[str, list[str]]] = {} | |
| def _cached(key: str, builder) -> dict[str, list[str]]: | |
| if key not in _CACHE: | |
| _CACHE[key] = builder() | |
| return _CACHE[key] | |
| def country_vocab() -> dict[str, list[str]]: | |
| return _cached("country", _country_entries) | |
| def state_vocab() -> dict[str, list[str]]: | |
| return _cached("state", lambda: _subdivision_entries("US")) | |
| def currency_vocab() -> dict[str, list[str]]: | |
| return _cached("currency", lambda: _currency_entries(60)) | |
| def _learnable_alias(alias: str, canon: str) -> bool: | |
| """Keep only aliases the model can LEARN as a string skill, not memorize as a lookup: | |
| variants (edit-similar), substrings/abbreviations, or acronyms. MEASURED: unfiltered | |
| arbitrary-knowledge aliases (BIA SAIGON->Sabeco) dilute training (-0.18 canon_f1).""" | |
| import difflib | |
| na = "".join(c.lower() for c in alias if c.isalnum()) | |
| nc = "".join(c.lower() for c in canon if c.isalnum()) | |
| if not na or not nc: | |
| return False | |
| if na in nc or nc in na: # substring/abbreviation | |
| return True | |
| initials = "".join(w[0].lower() for w in canon.split() if w and w[0].isalpha()) | |
| if len(na) >= 2 and na == initials: # acronym | |
| return True | |
| return difflib.SequenceMatcher(None, na, nc).ratio() >= 0.6 # variant | |
| def _alias_file(name: str, limit: int, seed: int = 3, | |
| learnable_only: bool = True) -> dict[str, list[str]]: | |
| """Load a harvested {canonical, aliases} jsonl (GeoNames/Wikidata/ROR) if present, | |
| gated to learnable aliases by default.""" | |
| import json as _json | |
| from pathlib import Path | |
| p = Path(__file__).resolve().parent.parent / "data" / name | |
| if not p.exists(): | |
| return {} | |
| rows = [_json.loads(l) for l in p.open(encoding="utf-8")] | |
| rng = random.Random(seed) | |
| rng.shuffle(rows) | |
| out: dict[str, list[str]] = {} | |
| for r in rows: | |
| canon = r["canonical"].strip() | |
| aliases = [a for a in r.get("aliases", []) if a and a != canon] | |
| if learnable_only: | |
| aliases = [a for a in aliases if _learnable_alias(a, canon)] | |
| if aliases and 3 <= len(canon) <= 60: | |
| out[canon] = aliases[:4] | |
| if len(out) >= limit: | |
| break | |
| return out | |
| def _city_entries() -> dict[str, list[str]]: | |
| # Curated cities carry valuable aliases (NYC->New York); the fetched open-data | |
| # list (training/cities.txt) adds breadth; harvested GeoNames English aliases | |
| # (data/geonames_city_aliases.jsonl, 80k cities / 152k aliases) add REAL variety. | |
| from pathlib import Path | |
| cities = list(CITIES) | |
| extra = Path(__file__).parent / "cities.txt" | |
| if extra.exists(): | |
| cities += [c.strip() for c in extra.read_text(encoding="utf-8").splitlines() if c.strip()] | |
| out = {c: list(_CITY_ALIASES.get(c, [])) for c in dict.fromkeys(cities)} | |
| for canon, aliases in _alias_file("geonames_city_aliases.jsonl", limit=4000).items(): | |
| merged = list(dict.fromkeys(out.get(canon, []) + aliases)) | |
| out[canon] = merged[:5] | |
| return out | |
| def company_vocab() -> dict[str, list[str]]: | |
| """Real company alias->canonical pairs (Wikidata skos:altLabel, CC0).""" | |
| return _cached("company", lambda: _alias_file("wikidata_company_aliases.jsonl", limit=2500)) | |
| def nickname_vocab() -> dict[str, list[str]]: | |
| """Unambiguous nickname->formal first-name pairs (carltonnorthern/nicknames, Apache-2.0).""" | |
| return _cached("nickname", lambda: _alias_file("nickname_aliases.jsonl", limit=600)) | |
| def city_vocab() -> dict[str, list[str]]: | |
| return _cached("city", _city_entries) | |
| def department_vocab() -> dict[str, list[str]]: | |
| return _cached("department", _department_entries) | |
| def _org_entries(limit: int = 2500, seed: int = 3) -> dict[str, list[str]]: | |
| """Real organization alias->canonical pairs harvested from the ROR registry | |
| (data/ror_aliases.jsonl, CC0; 72k orgs with aliases/acronyms): 'RMIT' -> | |
| 'RMIT University'. Empty dict (archetype skipped) when the harvest is absent.""" | |
| import json as _json | |
| from pathlib import Path | |
| p = Path(__file__).resolve().parent.parent / "data" / "ror_aliases.jsonl" | |
| if not p.exists(): | |
| return {} | |
| rows = [_json.loads(l) for l in p.open(encoding="utf-8")] | |
| rng = random.Random(seed) | |
| rng.shuffle(rows) | |
| out: dict[str, list[str]] = {} | |
| for r in rows: | |
| canon = r["canonical"].strip() | |
| aliases = [a.strip() for a in r.get("aliases", []) if a and a.strip() != canon] | |
| if not aliases or not (4 <= len(canon) <= 60): | |
| continue | |
| if sum(c.isascii() for c in canon) < 0.9 * len(canon): # keep learnable/ascii-ish | |
| continue | |
| out[canon] = aliases[:4] | |
| if len(out) >= limit: | |
| break | |
| return out | |
| def org_vocab() -> dict[str, list[str]]: | |
| return _cached("org", _org_entries) | |
| def job_title_vocab() -> dict[str, list[str]]: | |
| return _cached("job_title", _job_title_entries) | |
| def _industry_entries() -> dict[str, list[str]]: | |
| return { | |
| "Technology": ["Tech", "IT", "Software", "tech"], | |
| "Healthcare": ["Health Care", "Medical", "Health"], | |
| "Financial Services": ["Finance", "FinServ", "Banking", "Fintech"], | |
| "Retail": ["E-commerce", "Retail & E-commerce", "retail"], | |
| "Manufacturing": ["Mfg", "Industrial"], | |
| "Education": ["EdTech", "Ed", "education"], | |
| "Real Estate": ["RealEstate", "Property", "PropTech"], | |
| "Hospitality": ["Hotels & Travel", "Travel", "Tourism"], | |
| "Energy": ["Oil & Gas", "Utilities", "energy"], | |
| "Telecommunications": ["Telecom", "Telco", "Comms"], | |
| } | |
| def _unit_entries() -> dict[str, list[str]]: | |
| return { | |
| "kg": ["kilogram", "kilograms", "Kg", "KG"], | |
| "lb": ["lbs", "pound", "pounds", "Lb"], | |
| "cm": ["centimeter", "centimeters", "Cm"], | |
| "in": ["inch", "inches", "\""], | |
| "L": ["liter", "litre", "liters", "l"], | |
| "mL": ["milliliter", "ml", "mls"], | |
| "km": ["kilometer", "kilometers", "Km"], | |
| "mi": ["mile", "miles", "Mi"], | |
| } | |
| def industry_vocab() -> dict[str, list[str]]: | |
| return _cached("industry", _industry_entries) | |
| def unit_vocab() -> dict[str, list[str]]: | |
| return _cached("unit", _unit_entries) | |
| # Status / categorical value sets (canonical -> messy variants). | |
| _STATUS_SETS = [ | |
| {"Won": ["won", "WON", "Closed Won", "closed-won"], "Lost": ["lost", "Closed Lost"], | |
| "In Progress": ["in progress", "in-progress", "WIP", "ongoing"], "Open": ["open", "new"]}, | |
| {"High": ["high", "HIGH", "H", "P1"], "Medium": ["medium", "med", "M", "P2"], | |
| "Low": ["low", "L", "P3"]}, | |
| {"Active": ["active", "ACTIVE"], "Churned": ["churned", "cancelled", "canceled"], | |
| "Trial": ["trial", "TRIAL", "free trial"], "Paused": ["paused", "on hold"]}, | |
| {"Paid": ["paid", "PAID"], "Pending": ["pending", "unpaid", "due"], | |
| "Refunded": ["refunded", "refund"], "Overdue": ["overdue", "late"]}, | |
| ] | |