SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
11.8 KB · 203 lines python
Raw Blame History
1"""Industry taxonomy (registry/industries.yaml) and deterministic mapping of free-text industry labels to our slugs.23`map_industry(labels)` turns Wikidata P452 labels ("software industry", "banking"), SIC descriptions or any free text into an ordered,4de-duplicated list of taxonomy slugs. Matching is lexical only (no LLM): exact keyword match first, then the longest keyword phrase found5inside the label on word boundaries. `map_sic(code)` maps a US SIC code (SEC EDGAR) to a slug.6"""7from __future__ import annotations89import re10from dataclasses import dataclass, field11from functools import lru_cache12from pathlib import Path1314import yaml1516REGISTRY_DIR = Path(__file__).resolve().parents[3] / "registry"17INDUSTRIES_FILE = REGISTRY_DIR / "industries.yaml"181920@dataclass(frozen=True)21class Industry:22    slug: str23    name: str24    parent: str | None25    description: str26    keywords: tuple[str, ...]27    sort_order: int = 10028    children: tuple[str, ...] = field(default_factory=tuple)293031def _norm(text: str) -> str:32    text = text.lower().replace("_", " ").replace("&", " and ").replace("/", " ")33    text = re.sub(r"[^\w\s\-']", " ", text)34    return re.sub(r"\s+", " ", text).strip()353637@lru_cache38def load_industries(path: Path = INDUSTRIES_FILE) -> tuple[Industry, ...]:39    raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}40    rows = raw.get("industries") or []41    children: dict[str, list[str]] = {}42    for r in rows:43        if r.get("parent"):44            children.setdefault(r["parent"], []).append(r["slug"])45    out: list[Industry] = []46    for i, r in enumerate(rows):47        out.append(Industry(slug=r["slug"], name=r["name"], parent=r.get("parent"), description=(r.get("description") or "").strip(),48                            keywords=tuple(_norm(k) for k in (r.get("keywords") or []) if k), sort_order=(i + 1) * 10,49                            children=tuple(children.get(r["slug"], ()))))50    return tuple(out)515253@lru_cache54def industry_index() -> dict[str, Industry]:55    return {i.slug: i for i in load_industries()}565758def top_level_slugs() -> list[str]:59    return [i.slug for i in load_industries() if i.parent is None]606162def top_level_of(slug: str) -> str:63    ind = industry_index().get(slug)64    if ind is None:65        return slug66    return ind.parent or ind.slug676869def is_valid_slug(slug: str) -> bool:70    return slug in industry_index()717273@lru_cache74def _keyword_table() -> tuple[dict[str, tuple[str, ...]], tuple[tuple[str, re.Pattern[str], tuple[str, ...]], ...]]:75    """(exact keyword → slugs, [(keyword, boundary regex, slugs)] sorted by keyword length desc)."""76    exact: dict[str, list[str]] = {}77    for ind in load_industries():78        for kw in ind.keywords:79            exact.setdefault(kw, [])80            if ind.slug not in exact[kw]:81                exact[kw].append(ind.slug)82    phrases = []83    for kw, slugs in sorted(exact.items(), key=lambda kv: (-len(kv[0]), kv[0])):84        pat = re.compile(r"(?<![\w\-])" + re.escape(kw) + r"(?![\w\-])")85        phrases.append((kw, pat, tuple(slugs)))86    return {k: tuple(v) for k, v in exact.items()}, tuple(phrases)878889def map_label(label: str) -> list[str]:90    """Slugs for a single label. Exact keyword match, else the longest keyword phrase(s) contained in the label."""91    text = _norm(label)92    if not text:93        return []94    exact, phrases = _keyword_table()95    if text in exact:96        return list(exact[text])97    for variant in (text.removesuffix(" industry"), text.removesuffix(" company"), text.removesuffix(" sector"), text.removesuffix("s")):98        if variant != text and variant in exact:99            return list(exact[variant])100    best_len = 0101    found: list[str] = []102    for kw, pat, slugs in phrases:103        if best_len and len(kw) < best_len:104            break105        if pat.search(text):106            best_len = len(kw)107            for s in slugs:108                if s not in found:109                    found.append(s)110    return found111112113def map_industry(labels: str | list[str] | tuple[str, ...] | None, *, limit: int = 4) -> list[str]:114    """Ordered, de-duplicated taxonomy slugs for one or many labels (first label's matches come first)."""115    if not labels:116        return []117    if isinstance(labels, str):118        labels = [labels]119    out: list[str] = []120    for label in labels:121        for slug in map_label(label):122            if slug not in out:123                out.append(slug)124    return out[:limit]125126127# SIC (Standard Industrial Classification, as used by SEC EDGAR) → slug. Specific codes first, then ranges (inclusive).128_SIC_EXACT: dict[int, str] = {129    1311: "oil-gas", 1381: "oil-gas", 1382: "oil-gas", 1389: "oil-gas", 2111: "consumer-goods", 2834: "pharmaceuticals", 2835: "pharmaceuticals",130    2836: "biotechnology", 2833: "pharmaceuticals", 2911: "oil-gas", 3571: "technology", 3572: "cloud-infrastructure", 3576: "cloud-infrastructure",131    3577: "technology", 3578: "technology", 3661: "telecommunications", 3663: "telecommunications", 3669: "telecommunications",132    3674: "semiconductors", 3672: "semiconductors", 3679: "technology", 3630: "consumer-goods", 3634: "consumer-goods", 3651: "consumer-goods",133    3711: "automotive", 3713: "automotive", 3714: "automotive", 3715: "automotive", 3716: "automotive", 3751: "automotive",134    3720: "aerospace-defense", 3721: "aerospace-defense", 3724: "aerospace-defense", 3728: "aerospace-defense", 3760: "aerospace-defense",135    3812: "aerospace-defense", 3730: "manufacturing", 3743: "transportation", 3841: "medical-devices", 3842: "medical-devices",136    3843: "medical-devices", 3844: "medical-devices", 3845: "medical-devices", 3851: "medical-devices", 3942: "consumer-goods",137    3944: "consumer-goods", 3949: "consumer-goods", 4011: "transportation", 4013: "transportation", 4210: "logistics", 4213: "logistics",138    4400: "shipping", 4412: "shipping", 4512: "airlines", 4513: "logistics", 4522: "airlines", 4610: "oil-gas", 4700: "logistics",139    4731: "logistics", 4812: "telecommunications", 4813: "telecommunications", 4822: "telecommunications", 4832: "media", 4833: "media",140    4841: "media", 4899: "telecommunications", 4911: "utilities", 4922: "utilities", 4923: "utilities", 4924: "utilities", 4931: "utilities",141    4932: "utilities", 4941: "utilities", 4950: "utilities", 4953: "utilities", 4955: "utilities", 4991: "renewables", 5812: "food-beverage",142    5912: "retail", 5961: "e-commerce", 6021: "banking", 6022: "banking", 6029: "banking", 6035: "banking", 6036: "banking",143    6099: "payments", 6111: "financial-services", 6141: "financial-services", 6153: "financial-services", 6159: "financial-services",144    6162: "financial-services", 6163: "financial-services", 6172: "financial-services", 6189: "financial-services", 6199: "financial-services",145    6200: "financial-services", 6211: "financial-services", 6221: "financial-services", 6282: "asset-management", 6311: "insurance",146    6321: "insurance", 6324: "insurance", 6331: "insurance", 6351: "insurance", 6361: "insurance", 6399: "insurance", 6411: "insurance",147    6500: "real-estate", 6510: "real-estate", 6512: "real-estate", 6513: "real-estate", 6519: "real-estate", 6531: "real-estate",148    6552: "real-estate", 6770: "financial-services", 6792: "financial-services", 6794: "professional-services", 6795: "mining",149    6798: "real-estate", 6799: "asset-management", 7011: "hospitality", 7200: "professional-services", 7310: "media", 7311: "media",150    7320: "professional-services", 7330: "professional-services", 7331: "media", 7350: "professional-services", 7359: "professional-services",151    7361: "professional-services", 7363: "professional-services", 7370: "software", 7371: "software", 7372: "software", 7373: "software",152    7374: "cloud-infrastructure", 7377: "cloud-infrastructure", 7380: "professional-services", 7381: "professional-services",153    7384: "professional-services", 7385: "telecommunications", 7389: "professional-services", 7500: "automotive", 7510: "automotive",154    7812: "entertainment", 7819: "entertainment", 7822: "entertainment", 7829: "entertainment", 7830: "entertainment", 7841: "entertainment",155    7900: "entertainment", 7948: "entertainment", 7990: "entertainment", 7997: "hospitality", 8000: "healthcare", 8011: "healthcare",156    8050: "healthcare", 8051: "healthcare", 8060: "healthcare", 8062: "healthcare", 8071: "healthcare", 8082: "healthcare", 8090: "healthcare",157    8093: "healthcare", 8111: "professional-services", 8200: "education", 8300: "healthcare", 8351: "education", 8600: "professional-services",158    8700: "professional-services", 8711: "professional-services", 8721: "professional-services", 8731: "biotechnology",159    8734: "professional-services", 8741: "professional-services", 8742: "consulting", 8744: "professional-services", 8748: "consulting",160    8880: "financial-services", 8888: "financial-services", 8900: "professional-services", 9995: "financial-services",161}162_SIC_RANGES: tuple[tuple[int, int, str], ...] = (163    (100, 999, "agriculture"), (1000, 1299, "mining"), (1300, 1399, "oil-gas"), (1400, 1499, "mining"), (1500, 1799, "construction"),164    (2000, 2099, "food-beverage"), (2100, 2199, "consumer-goods"), (2200, 2399, "apparel"), (2400, 2499, "materials"), (2500, 2599, "consumer-goods"),165    (2600, 2699, "materials"), (2700, 2799, "media"), (2800, 2829, "chemicals"), (2830, 2839, "pharmaceuticals"), (2840, 2899, "chemicals"),166    (2900, 2999, "oil-gas"), (3000, 3099, "chemicals"), (3100, 3199, "apparel"), (3200, 3299, "materials"), (3300, 3399, "mining"),167    (3400, 3499, "manufacturing"), (3500, 3569, "industrial-machinery"), (3570, 3579, "technology"), (3580, 3599, "industrial-machinery"),168    (3600, 3629, "manufacturing"), (3630, 3639, "consumer-goods"), (3640, 3659, "manufacturing"), (3660, 3669, "telecommunications"),169    (3670, 3679, "semiconductors"), (3680, 3699, "technology"), (3700, 3719, "automotive"), (3720, 3729, "aerospace-defense"),170    (3730, 3739, "manufacturing"), (3740, 3749, "transportation"), (3750, 3759, "automotive"), (3760, 3769, "aerospace-defense"),171    (3770, 3799, "manufacturing"), (3800, 3839, "technology"), (3840, 3859, "medical-devices"), (3860, 3899, "technology"),172    (3900, 3999, "consumer-goods"), (4000, 4099, "transportation"), (4100, 4199, "transportation"), (4200, 4299, "logistics"),173    (4300, 4399, "logistics"), (4400, 4499, "shipping"), (4500, 4599, "airlines"), (4600, 4699, "oil-gas"), (4700, 4799, "logistics"),174    (4800, 4829, "telecommunications"), (4830, 4849, "media"), (4850, 4899, "telecommunications"), (4900, 4999, "utilities"),175    (5000, 5199, "retail"), (5200, 5799, "retail"), (5800, 5899, "food-beverage"), (5900, 5999, "retail"), (6000, 6099, "banking"),176    (6100, 6199, "financial-services"), (6200, 6299, "financial-services"), (6300, 6499, "insurance"), (6500, 6599, "real-estate"),177    (6700, 6799, "asset-management"), (7000, 7099, "hospitality"), (7200, 7299, "professional-services"), (7300, 7369, "professional-services"),178    (7370, 7379, "software"), (7380, 7399, "professional-services"), (7500, 7599, "automotive"), (7600, 7699, "professional-services"),179    (7800, 7899, "entertainment"), (7900, 7999, "entertainment"), (8000, 8099, "healthcare"), (8100, 8199, "professional-services"),180    (8200, 8299, "education"), (8300, 8399, "healthcare"), (8400, 8499, "entertainment"), (8600, 8699, "professional-services"),181    (8700, 8799, "professional-services"), (8800, 8999, "professional-services"),182)183184185def map_sic(code: int | str | None) -> str | None:186    """US SIC code → taxonomy slug (None when unknown/blank)."""187    if code in (None, ""):188        return None189    try:190        n = int(str(code).strip())191    except ValueError:192        return None193    if n in _SIC_EXACT:194        return _SIC_EXACT[n]195    for lo, hi, slug in _SIC_RANGES:196        if lo <= n <= hi:197            return slug198    return None199200201__all__ = ["INDUSTRIES_FILE", "REGISTRY_DIR", "Industry", "industry_index", "is_valid_slug", "load_industries", "map_industry",202           "map_label", "map_sic", "top_level_of", "top_level_slugs"]203