Skip to content

[FEATURE] AI-gestützte Konzernzugehörigkeit für Zefix-Unternehmen #4

Description

@fabricevonaarburg

Labels: feature-request, AI, data-enrichment


name: Feature Request — AI-gestützte Konzernzugehörigkeit about: Neue Funktion zur automatischen Erkennung und Anzeige von Konzernstrukturen title: "[FEATURE] AI-gestützte Konzernzugehörigkeit für Zefix-Unternehmen" labels: feature-request, AI, data-enrichment, companies-view assignees: ''

💡 Feature Summary

Es soll eine neue Funktion entwickelt werden, die mithilfe von AI automatisch die Konzernzugehörigkeit von Schweizer Unternehmen aus dem Zefix erkennt und zuordnet. In der Unternehmensansicht soll ein neues Filterfeld „Konzernzugehörigkeit" erscheinen, das es ermöglicht, alle Gesellschaften eines Konzerns gesammelt anzuzeigen — z.B. werden A AG, B AG und C AG dem A Konzern zugeordnet.


Motivation / Use Case

Viele Schweizer Konzerne bestehen aus Dutzenden oder Hunderten von rechtlich eigenständigen Gesellschaften, die im Zefix als separate Einträge erscheinen. Für Prüfer, Compliance-Teams und Analysten ist es heute sehr aufwändig, alle Gesellschaften eines Konzerns manuell zu identifizieren.

Beispiel:

Nestlé SA, Nestlé Suisse SA, Nestlé Products SA, Nestlé Research AG → alle gehören zum Nestlé Konzern

Mit dieser Funktion können Nutzer:

  • Alle Konzerngesellschaften auf einen Blick sehen
  • Konzernweite Risiken, Revisionsstellen und Verwaltungsräte analysieren
  • Due-Diligence-Prüfungen konzernweit durchführen

Gewünschtes Verhalten (User Story)

Als Wirtschaftsprüfer / Compliance-Analyst
möchte ich bei der Unternehmenssuche nach Konzernzugehörigkeit filtern können
damit ich alle Gesellschaften eines Konzerns vollständig und effizient analysieren kann.

UI — Neue Filteroption „Konzernzugehörigkeit"

Unternehmen
├── Filter: Kanton         [Luzern ▼]
├── Filter: Rechtsform     [AG ▼]
├── Filter: Revisionsstelle [PwC ▼]
└── Filter: Konzernzugehörigkeit  [Nestlé Konzern ▼]  ← NEU

Ergebnisse (4 Unternehmen):
┌──────────────────────────────────────────────────────┐
│ Nestlé SA CHE-100.349.897 Aktiv │
│ Nestlé Suisse SA CHE-106.212.193 Aktiv │
│ Nestlé Products SA CHE-102.933.012 Aktiv │
│ Nestlé Research AG CHE-108.442.671 Aktiv │
└──────────────────────────────────────────────────────┘


Technische Lösung

Übersicht der Architektur

Zefix Rohdaten
     │
     ▼
[1] Signale extrahieren
(Namensähnlichkeit, gemeinsame VR-Mitglieder,
 Adresse, Beteiligungsstruktur)
     │
     ▼
[2] AI-Clustering (LLM + Graph-Algorithmus)
     │
     ▼
[3] Konzern-Tabelle befüllen
(konzern_name, confidence_score, methode)
     │
     ▼
[4] API-Endpoint + UI-Filter bereitstellen

Schritt 1 — Datenbank-Schema erweitern

-- Neue Tabelle: Konzerne
CREATE TABLE corporate_groups (
  id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name          TEXT NOT NULL,           -- z.B. "Nestlé Konzern"
  parent_uid    TEXT REFERENCES companies(uid),  -- Muttergesellschaft
  created_at    TIMESTAMPTZ DEFAULT NOW(),
  updated_at    TIMESTAMPTZ DEFAULT NOW(),
  source        TEXT DEFAULT 'ai',       -- 'ai' | 'manual' | 'ownership'
  confidence    FLOAT DEFAULT 0.0        -- 0.0 – 1.0
);

-- Neue Spalte in companies-Tabelle
ALTER TABLE companies
ADD COLUMN group_id UUID REFERENCES corporate_groups(id),
ADD COLUMN group_confidence FLOAT DEFAULT NULL;

-- Index für schnelle Filter-Abfragen
CREATE INDEX idx_companies_group_id ON companies(group_id);


Schritt 2 — AI-Clustering-Pipeline

Die Pipeline läuft täglich (via Cron-Job) und kombiniert drei Methoden:

Methode A: Namensähnlichkeit (Heuristik)

import re
from difflib import SequenceMatcher

def extract_group_name_candidate(company_name: str) -> str:
"""
Extrahiert den Kernnahmen aus einem Firmennamen.
'Nestlé Suisse SA' → 'Nestlé'
'PricewaterhouseCoopers AG' → 'PricewaterhouseCoopers'
"""
# Entferne Rechtsformen
suffixes = r'\b(AG|SA|GmbH|Sàrl|Ltd|KG|OHG|Genossenschaft|Stiftung)\b'
cleaned = re.sub(suffixes, '', company_name, flags=re.IGNORECASE).strip()
# Erstes Wort als Kandidat
return cleaned.split()[0] if cleaned else company_name

def similarity(a: str, b: str) -> float:
return SequenceMatcher(None, a.lower(), b.lower()).ratio()

Methode B: Gemeinsame Verwaltungsratsmitglieder (Graph)

import networkx as nx

def build_board_overlap_graph(companies: list[dict]) -> nx.Graph:
"""
Verbindet Unternehmen, die ≥1 gemeinsames VR-Mitglied haben.
Starke Verbindungen (≥2 gemeinsame Personen) = wahrscheinlich Konzern.
"""
G = nx.Graph()
person_to_companies = {} # person_uid → [company_uid, ...]

for company in companies:
    for person in company.get('board_members', []):
        pid = person['uid']
        if pid not in person_to_companies:
            person_to_companies[pid] = []
        person_to_companies[pid].append(company['uid'])

for person_uid, company_uids in person_to_companies.items():
    for i in range(len(company_uids)):
        for j in range(i + 1, len(company_uids)):
            u, v = company_uids[i], company_uids[j]
            if G.has_edge(u, v):
                G[u][v]['weight'] += 1
            else:
                G.add_edge(u, v, weight=1)

return G

Methode C: AI-Klassifizierung via LLM (Claude API)

import anthropic

client = anthropic.Anthropic()

def classify_group_with_ai(company_names: list[str]) -> dict:
"""
Nutzt Claude, um Firmennamen einem Konzern zuzuordnen.
"""
prompt = f"""
Du bist ein Experte für Schweizer Unternehmensstrukturen.

Analysiere diese Firmennamen aus dem Zefix und gruppiere sie nach Konzernzugehörigkeit.
Antworte NUR mit validem JSON. Format:
{{
  "groups": [
    {{
      "konzern_name": "Name des Konzerns",
      "parent_company": "Name der Muttergesellschaft oder null",
      "members": ["Firma A", "Firma B"],
      "confidence": 0.95,
      "reasoning": "Kurze Begründung"
    }}
  ]
}}

Firmennamen:
{chr(10).join(f'- {name}' for name in company_names)}
"""

message = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=2000,
    messages=[{"role": "user", "content": prompt}]
)

import json
return json.loads(message.content[0].text)


Schritt 3 — Neuer API-Endpoint

// GET /v1/groups — alle Konzerne auflisten
// GET /v1/groups/{id} — Konzerndetails + alle Mitglieder
// GET /v1/groups/{id}/companies — alle Gesellschaften eines Konzerns
// POST /v1/groups/{id}/merge — zwei Konzerne manuell zusammenführen
// PUT /v1/companies/{uid}/group — manuelle Zuweisung

// Erweiterung des bestehenden /v1/companies Endpoints:
// GET /v1/companies?group_id=<uuid> ← neuer Filter-Parameter

/**

  • Beispiel-Response für GET /v1/groups/abc-123
    */
    {
    "id": "abc-123",
    "name": "Nestlé Konzern",
    "parent_uid": "CHE-100.349.897",
    "parent_name": "Nestlé SA",
    "member_count": 47,
    "confidence": 0.94,
    "source": "ai",
    "companies": [
    {
    "uid": "CHE-100.349.897",
    "name": "Nestlé SA",
    "canton": "VD",
    "status": "Active",
    "is_parent": true
    },
    {
    "uid": "CHE-106.212.193",
    "name": "Nestlé Suisse SA",
    "canton": "VD",
    "status": "Active",
    "is_parent": false
    }
    // ...
    ]
    }

Schritt 4 — UI-Anpassung (Companies-Seite)

// Neues Filter-Feld in der Companies-Ansicht

// 1. Autocomplete-Suche für Konzernname
<FilterField
label="Konzernzugehörigkeit"
placeholder="z.B. Nestlé, Zurich Insurance, UBS..."
endpoint="/api/v1/groups?search={query}"
onSelect={(group) => applyFilter('group_id', group.id)}
/>

// 2. Anzeige in der Unternehmenstabelle
<TableColumn
header="Konzern"
render={(company) =>
company.group ? (
<Badge
label={company.group.name}
onClick={() => applyFilter('group_id', company.group.id)}
confidence={company.group_confidence}
/>
) : (
<span className="text-muted">—</span>
)
}
/>

// 3. Konzernübersicht-Karte beim Klick auf einen Konzern
<GroupDetailPanel group={selectedGroup}>
<GroupHierarchyTree /> // Visualisierung der Konzernstruktur
<GroupCompanyTable /> // Tabelle aller Gesellschaften
<GroupRiskSummary /> // Aggregierter Risk Score
<GroupAuditorOverview /> // Revisionsstellen im Konzern
</GroupDetailPanel>


Schritt 5 — Cron-Job für tägliche Aktualisierung

# Läuft täglich nach dem Zefix-Sync
# Pseudocode für die Pipeline

async def update_corporate_groups():
# 1. Alle Unternehmen laden
companies = await db.fetch("SELECT uid, name, board_members FROM companies")

# 2. Graph-Methode: Board-Overlap
G = build_board_overlap_graph(companies)
graph_clusters = detect_communities(G, min_weight=2)

# 3. Namens-Methode: Gruppierung nach Kernname
name_clusters = cluster_by_name_similarity(companies, threshold=0.75)

# 4. Bestehende Ownership-Daten aus /v1/relationships einbeziehen
ownership_groups = await fetch_ownership_clusters()

# 5. Alle Signale zusammenführen
merged = merge_cluster_signals(graph_clusters, name_clusters, ownership_groups)

# 6. AI-Validierung für unsichere Cluster (confidence &lt; 0.8)
uncertain = [c for c in merged if c.confidence &lt; 0.8]
if uncertain:
    ai_results = await classify_group_with_ai([c.names for c in uncertain])
    apply_ai_corrections(merged, ai_results)

# 7. Datenbank aktualisieren
await upsert_groups(merged)

print(f"✅ {len(merged)} Konzerne aktualisiert")


Credit-Kosten (Vorschlag)

Operation Credits
GET /v1/groups (Liste) 1
GET /v1/groups/{id} (Detail) 2
GET /v1/companies?group_id=... 1
POST /v1/ai/group-classify (manuelle AI-Anfrage) 10

Akzeptanzkriterien

  • Neue Tabelle corporate_groups in der Datenbank
  • Tägliche AI-Clustering-Pipeline läuft zuverlässig
  • Filter „Konzernzugehörigkeit" erscheint in der Companies-Ansicht
  • Autocomplete-Suche nach Konzernname funktioniert
  • GET /v1/companies?group_id=<uuid> gibt korrekte Ergebnisse
  • GET /v1/groups/{id} gibt alle Mitglieder mit Confidence-Score zurück
  • Manuelle Korrektur der Zuweisung ist möglich (PUT endpoint)
  • Confidence-Score wird im UI angezeigt (z.B. als Tooltip)
  • Bestehende Relationships/Ownership-Daten werden als Signal genutzt

Zusätzliche Hinweise

  • VynCo nutzt bereits /v1/companies/{uid}/relationships und /v1/companies/{uid}/hierarchy — diese Daten sollten als erstes Signal genutzt werden, bevor AI eingesetzt wird
  • Der Confidence-Score erlaubt es Nutzern, die Zuverlässigkeit einer Zuordnung einzuschätzen
  • Eine manuelle Override-Funktion ist wichtig, damit Nutzer fehlerhafte AI-Zuordnungen korrigieren können
  • Langfristig könnte dies zu einem eigenen „Konzernanalyse"-Dashboard ausgebaut werden

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions