JevCode / Ökosystem-Beispiele

Klassifizierung mit Konfidenz

Klassifiziere SEC-Jahresberichte in 75 Industriegruppen mit jeweils einem Choice, und lies dann das eigene Konfidenzniveau der Antwort, um zu entscheiden, ob diese Gruppe oder die darüberliegende breitere Division gemeldet wird.

Maschinell aus en übersetzt, nicht lektoriert. Nur als Kurzreferenz geeignet.

Quelle: docs.typesafe.ai/cookbooks/classification_using_confidencecookbookrecipe
One confident pick among many candidates

Jede Firma, die einen Jahresbericht bei der SEC einreicht, beschreibt darin ihr eigenes Geschäft. Wir klassifizieren diese Beschreibungen unter der Standard Industrial Classification: 75 Industrie gruppen, eine Choice Frage pro Dokument.

Die meisten Einreichungen sind einfach. Eine Regionalbank ist eine Regionalbank. Einige sind es nicht: ein Unternehmen, das gerade eines seiner beiden Segmente verkauft hat, oder ein Startup, das ein Geschäft beschreibt, das es betreten will, anstatt eines, das es betreibt. Das Modell muss unabhängig davon eine Gruppe auswählen, und die Antwort für einen schwierigen Fall sieht nicht anders aus als die Antwort für einen einfachen Fall. Die Unterscheidung zwischen schwierigen und einfachen Fällen ist normalerweise dort, wo die Kosten entstehen: ein zweites Modell, zusätzliche Aufrufe, menschliche Überprüfung.

Ein Choice sagt es dir bereits. Neben der siegreichen Option gibt er confidence zurück, hoch, wenn fast die gesamte Wahrscheinlichkeit auf einer Option liegt, und niedrig, wenn sie sich auf mehrere verteilt. Diese eine Zahl trennt die Antworten, denen du vertrauen kannst, von denen, denen du es nicht kannst.

Was man mit einer nicht vertrauenswürdigen Antwort macht, hängt von Ihren Labels ab. SIC-Labels bilden eine Hierarchie: Industriegruppen werden zu weiteren Abteilungen zusammengefasst. Das macht eine Antwort nahezu kostenlos. Wenn das Modell sich der Gruppe nicht sicher ist, melden Sie die Abteilung, der es angehört. Das breite Label ergibt sich aus dem schmalen, also gibt es keinen zweiten Aufruf.

Über 60 Einreichungen hinweg teilt ein Konfidenz-Schwellenwert von 0,9 sie in zwei Hälften. Die sichere Hälfte liegt zu 90 % richtig; die andere Hälfte zu 40 %. Auf einer Ebene höher berichtet, wird diese 40 % zu 70 %. Wir enden mit einer classify()-Funktion, die ein Label sowie die Spezifität zurückgibt, bei einer Anfrage pro Dokument.

Flussrichtung: LR

Knoten Beschreibung Gruppe
doc Artikel 1 ‘Business’ / aus einem 10-K —
request eine Anfrage eine Anfrage
q Choice / 75 Industriegruppen eine Anfrage
sure Vertrauen / ≥ 0,9? —
grp melden Sie die Industriegruppe / z.B. 28 —
div melden Sie ihre Abteilung / z.B. Fertigung —
Von Bedingung Zu
doc — request
sure ja grp
sure nein div

Setup

pip install ipython matplotlib "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

dann TYPESAFE_API_KEY festlegen. Jeder API-Aufruf wird in json_cache.json zwischengespeichert, das mit dem Kochbuch ausgeliefert wird, sodass das erneute Rendern die veröffentlichten Zahlen abspielt, ohne die API aufzurufen. Löschen Sie diese Datei, um alles live neu auszuführen.

Die untenstehenden Zahlen stammen von jev-1.12 am 2026-08-12.

import json
from collections import defaultdict
from pathlib import Path

import matplotlib
import matplotlib.pyplot as plt
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Choice, TypeSafeClient

matplotlib.use("Agg")  # headless render

import os  # noqa: E402

TYPESAFE_MODEL = "jev-1.12"
CONFIDENT = 0.9  # above this the group is reported; below it, the division

client = TypeSafeClient(
    api_key=os.environ.get(
        "TYPESAFE_API_KEY", "cache-only"
    ),  # keyless kernels replay the cache
    base_url=os.environ.get("TYPESAFE_ENDPOINT"),
    timeout=120.0,
)
json_cache = JsonCache(Path("json_cache.json"))

Erstellen Sie die beiden Ebenen der Taxonomie

sic_codes.tsv ist die Branchenliste, die die SEC für Meldepflichtige veröffentlicht, aus der sie ihren eigenen Code auswählen können, abgerufen am 2026-08-10: 444 vierstellige Codes, jeweils mit einem Branchen-Titel. Die Ziffern bilden eine Hierarchie. Die ersten beiden stellen die Hauptgruppe dar (75 davon hier, von 01 landwirtschaftliche Produktion bis 99 nicht klassifizierbar), und feste Bereiche von Hauptgruppen bilden die zehn Abteilungen, die breiteste Unterteilung des SIC.

Beide Ebenen stammen aus dieser einen Datei ohne Beteiligung eines Modells: Gruppieren Sie die Codes nach ihren ersten beiden Ziffern und weisen Sie diese Ziffern dann einer Division zu.

DIVISIONS = [
    (1, 9, "agriculture, forestry and fishing"),
    (10, 14, "mining"),
    (15, 17, "construction"),
    (20, 39, "manufacturing"),
    (40, 49, "transportation, communications and utilities"),
    (50, 51, "wholesale trade"),
    (52, 59, "retail trade"),
    (60, 67, "finance, insurance and real estate"),
    (70, 89, "services"),
    (91, 99, "public administration"),
]

INDUSTRIES: dict[str, str] = {}
for line in Path("sic_codes.tsv").read_text().splitlines()[1:]:
    code, _office, title = line.split("\t")
    INDUSTRIES[code] = title.lower()

GROUPS: dict[str, list[str]] = defaultdict(list)
for code in sorted(INDUSTRIES):
    GROUPS[code[:2]].append(code)


def division(group: str) -> str:
    number = int(group)
    return next(name for low, high, name in DIVISIONS if low <= number <= high)


print(
    f"{len(INDUSTRIES)} industries -> {len(GROUPS)} major groups -> {len(DIVISIONS)} divisions"
)
print(
    f"  group 35 = {division('35')} / {', '.join(INDUSTRIES[c] for c in GROUPS['35'][:3])} ..."
)
444 industries -> 75 major groups -> 10 divisions
  group 35 = manufacturing / engines & turbines, farm machinery & equipment, lawn & garden tractors & home lawn & gardens equip ...

Eine Choice-Frage benötigt etwas, um jede Option zu beschreiben, und der Eigenname einer Gruppe ist nicht immer vorhanden: 42 der 75 tragen einen Oberbegriff in der Liste der SEC, und die anderen tragen keinen. Daher wird jede Gruppe durch die Branchen innerhalb dieser Gruppe beschrieben, was jemand, der die Einreichung liest, sowieso abgleichen würde.

MAX_NAMED = (
    8  # industries listed per group; enough to characterise it without a wall of text
)


def describe(group: str) -> str:
    umbrella = INDUSTRIES.get(f"{group}00")
    inside = [INDUSTRIES[c] for c in GROUPS[group] if c != f"{group}00"][:MAX_NAMED]
    listed = "; ".join(inside)
    return (
        f"{umbrella} — includes: {listed}"
        if umbrella and listed
        else (umbrella or listed)
    )


print(f"group 20: {describe('20')[:150]}")
print(f"\ngroup 65: {describe('65')[:150]}")
group 20: food and kindred products — includes: meat packing plants; sausages & other prepared meat products; poultry slaughtering and processing; dairy product

group 65: real estate — includes: real estate operators (no developers) & lessors; operators of nonresidential buildings; operators of apartment buildings; less

Die Einreichungen

filings.jsonl enthält 60 Jahresberichte (10-K), die jeweils auf Abschnitt 1 „Business“ gekürzt wurden, den Abschnitt, in dem ein Unternehmen beschreibt, was es tut; dies ist der einzige Teil, der für einen Industriecode relevant ist. Sie umfassen den Zeitraum 1993–2024 und reichen von 700 bis 2.200 Wörtern. Jeder enthält den SIC-Code, den der Einreicher gewählt hat, sowie die Zugangsnummer zur Suche auf EDGAR.

Woher dieses Label stammt, ist vor jeder Genauigkeitszahl entscheidend. Es wird selbst angegeben: Wer auch immer den Antrag vorbereitet hat, hat ihn einmal ausgewählt, und er veraltet, wenn ein Unternehmen das Geschäft verkauft, dessen Codenamen es trägt, und die Codenamen behält. Diese 60 wurden auf Anträge reduziert, deren eigener Text den von ihnen getragenen Code unterstützt, sodass die Zahlen hier das Rezept messen und nicht den Zustand von EDGARS Metadaten.

FILINGS = [json.loads(line) for line in Path("filings.jsonl").read_text().splitlines()]
example = FILINGS[7]
print(
    f"{len(FILINGS)} filings, {sum(f['words'] for f in FILINGS) // len(FILINGS)} words on average"
)
print(f"\n{example['id']} (filed {example['year']}, accession {example['accession']}):")
print(f"  {example['text'][:230]}...")
print(f"  filer's code: {example['sic']} {INDUSTRIES[example['sic']]}")
60 filings, 1438 words on average

1389870_2008 (filed 2008, accession 0001079974-09-000155):
  Item 1. DESCRIPTION OF BUSINESS. NARRATIVE DESCRIPTION OF THE BUSINESS Across America Financial Services, Inc. is a corporation which was formed under the laws of the State of Colorado on December 1, 2005. Until March 23, 2007, we...
  filer's code: 6163 loan brokers

Stelle eine Choice-Frage und lies das Vertrauen ab

Eine Choice Frage, deren Optionen die 75 Gruppen sind. Die gesamte Taxonomie passt in eine Anfrage: Ein Choice funktioniert zuverlässig bis zu etwa 240 Optionen, und 75 liegt weit innerhalb dieses Bereichs.

Die Antwort kommt mit choice, die gewinnende Gruppe; probabilities, das Gewicht auf jedem der 75; und confidence, das angibt, wie konzentriert diese Verteilung war. Das Rezept liest confidence statt der eigenen Wahrscheinlichkeit des Gewinners. Ein Gewinner bei 0.45 mit einem Zweiten bei 0.44 und ein Gewinner bei 0.45, wobei das restliche Gewicht dünn verteilt ist, sind unterschiedliche Situationen, und confidence ist das, was sie trennt.

QUESTION = (
    "Which broad industry does this company operate in? Judge the company's own operations "
    "as this filing describes them."
)


def questions() -> dict:
    return {
        "group": Choice(
            instructions=QUESTION,
            criteria={group: describe(group) for group in sorted(GROUPS)},
        )
    }


@json_cache
def ask(filing_id: str, text: str) -> dict:
    response = client.system_one(
        state=text, questions=questions(), model=TYPESAFE_MODEL
    )
    answer = response.answers["group"]
    return {
        "group": answer.choice,
        "confidence": answer.confidence,
        "probabilities": dict(answer.probabilities),
    }

Gib die Gruppe zurück, wenn du sicher bist, ihre Abteilung, wenn nicht

Die vier Zeilen unten sind das gesamte Rezept. Bei einem Konfidenzwert von 0,9 oder höher wird die Antwort als Industriegruppe gemeldet; darunter wird dieselbe Antwort als die Abteilung gemeldet, in der diese Gruppe angesiedelt ist.

Jede Einreichung kommt mit einem verwertbaren Label zurück. Eine, die das Modell nicht sicher klassifizieren konnte, kommt eine Ebene höher zurück, anstatt verworfen oder weitergeleitet zu werden. Wenn eine Aufteilung für Ihre Anwendung zu grob ist, um darauf zu reagieren, ist dies der Zweig, an den Sie sie an eine Person übergeben.

def classify(filing: dict) -> dict:
    answer = ask(filing["id"], filing["text"])
    sure = answer["confidence"] >= CONFIDENT
    return {
        "level": "group" if sure else "division",
        "label": answer["group"] if sure else division(answer["group"]),
        "confidence": answer["confidence"],
        "group": answer["group"],
    }


def show(filing: dict) -> None:
    result = classify(filing)
    named = describe(result["group"]).split(" — ")[0][:46]
    print(
        f"  {filing['id']:>13}  conf {result['confidence']:.2f}  -> {result['level']:<8} "
        f"{result['label']:<14} (group {result['group']}: {named})"
    )


print("three filings the model was sure about:")
for f in sorted(FILINGS, key=lambda f: -ask(f["id"], f["text"])["confidence"])[:3]:
    show(f)
print("\nthree it was not:")
for f in sorted(FILINGS, key=lambda f: ask(f["id"], f["text"])["confidence"])[:3]:
    show(f)
three filings the model was sure about:
    310158_1996  conf 1.00  -> group    28             (group 28: chemicals & allied products)
     33416_1998  conf 1.00  -> group    63             (group 63: life insurance; accident & health insurance; h)
    352541_1996  conf 1.00  -> group    49             (group 49: electric, gas & sanitary services)

three it was not:
   1372167_2013  conf 0.22  -> division manufacturing  (group 38: search, detection, navagation, guidance, aeron)
   1398633_2009  conf 0.23  -> division wholesale trade (group 50: wholesale-durable goods)
     46653_1999  conf 0.29  -> division services       (group 87: services-engineering, accounting, research, ma)

Die Konfidenzwerte stimmen damit überein, wie schwer jede Einreichung zu klassifizieren ist. Die drei mit 1.00 sind ein pharmazeutischer Hersteller, eine Lebensversicherung und ein Versorgungsunternehmen; alle drei sind auf dem Papier Holdinggesellschaften, aber jede hat ein dominantes Geschäftsgebiet, das die Einreichung offen benennt. Die drei am unteren Ende sind aus Gründen schwieriger, die Sie im Text nachlesen können. Zwei sind Entwicklungsunternehmen, die ein Geschäft beschreiben, das sie starten beabsichtigen (Nevaeh “beabsichtigt, als Softwareentwickler zu operieren”), Barricode wurde “organisiert, um in die Computer-Sicherheitssoftware-Branche einzutreten”), und das dritte hatte zwei Segmente und verkaufte eines davon Wochen vor der Einreichung. Diese drei kommen als Abteilung statt als Gruppe zurück.

classify() ist das gesamte Rezept. Zeige ask() auf deine eigenen Dokumente und schreibe describe() für deine eigene Taxonomie um, und der Rest wird übernommen.

Was die umfassendere Antwort bringt

Alle 60 Einreichungen, bewertet gegen den von jedem Einreicher gewählten Code, unter beiden Richtlinien: Nenne eine Gruppe jedes Mal, oder melde die Aufteilung, sobald das Vertrauen unter 0,9 fällt.

def correct(filing: dict, result: dict) -> bool:
    gold_group = filing["sic"][:2]
    if result["level"] == "group":
        return result["label"] == gold_group
    return result["label"] == division(gold_group)


results = [(f, classify(f)) for f in FILINGS]
sure = [(f, r) for f, r in results if r["level"] == "group"]
unsure = [(f, r) for f, r in results if r["level"] == "division"]

forced = sum(r["group"] == f["sic"][:2] for f, r in results)
broadened = sum(correct(f, r) for f, r in results)

print(f"forced to name a group every time      {forced}/{len(results)} right")
print(
    f"  of those, the {len(sure)} it was sure about  "
    f"{sum(r['group'] == f['sic'][:2] for f, r in sure)}/{len(sure)} right"
)
print(
    f"  and the {len(unsure)} it was not           "
    f"{sum(r['group'] == f['sic'][:2] for f, r in unsure)}/{len(unsure)} right"
)
print(
    f"\nletting it answer coarsely when unsure  {broadened}/{len(results)} useful answers"
)
forced to name a group every time      39/60 right
  of those, the 30 it was sure about  27/30 right
  and the 30 it was not           12/30 right

letting it answer coarsely when unsure  48/60 useful answers

Wo das Modell sicher war, ist die von ihm genannte Gruppe neun von zehn Mal richtig. Wo es nicht sicher war, war die Nennung einer Gruppe häufiger falsch als richtig, bei 40 %. Die Berichterstattung über dieselben Antworten als Division bringt sie auf 70 %.

Die Grafik stellt die beiden Richtlinien nebeneinander aufgeteilt, je nachdem, ob das Modell sich sicher war.

labels = ["sure\n(group reported)", "unsure\n(division reported)"]
forced_split = [
    sum(r["group"] == f["sic"][:2] for f, r in sure) / len(sure),
    sum(r["group"] == f["sic"][:2] for f, r in unsure) / len(unsure),
]
broad_split = [
    sum(correct(f, r) for f, r in sure) / len(sure),
    sum(correct(f, r) for f, r in unsure) / len(unsure),
]

fig, ax = plt.subplots(figsize=(7, 3.6))
x = range(len(labels))
ax.bar(
    [i - 0.19 for i in x],
    forced_split,
    0.38,
    label="always name a group",
    color="#c8ccd4",
)
ax.bar(
    [i + 0.19 for i in x],
    broad_split,
    0.38,
    label="answer broadly when unsure",
    color="#3b6ea5",
)
for i, (a, b) in enumerate(zip(forced_split, broad_split)):
    ax.text(i - 0.19, a + 0.02, f"{a:.0%}", ha="center", fontsize=9)
    ax.text(i + 0.19, b + 0.02, f"{b:.0%}", ha="center", fontsize=9)
ax.set_xticks(list(x))
ax.set_xticklabels(
    [f"{lab}\nn={n}" for lab, n in zip(labels, [len(sure), len(unsure)])]
)
ax.set_ylabel("labels that are right")
ax.set_ylim(0, 1.12)
ax.set_title("Where the broader answer helps: the filings it was unsure about")
ax.legend(frameon=False, loc="upper right")
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
display(fig)
output

Öffnen Sie es im Playground

Dieser Freigabelink enthält eine Einreichung und die Frage mit 75 Optionen, sodass Sie die Verteilung und das daraus resultierende Konfidenzniveau sehen können, ohne Code schreiben zu müssen.

playground_link = make_playground_link(
    example["text"], questions(), models=[TYPESAFE_MODEL]
)
display(
    Markdown(
        f"🔗 [Open the filing + question in the TypeSafe playground]({playground_link})"
    )
)

Öffne die Einreichung + Frage im TypeSafe-Playground →