From d723c36c3aafda8064a00a3ceb6287a88be69468 Mon Sep 17 00:00:00 2001 From: Floke Date: Thu, 18 Sep 2025 12:00:04 +0000 Subject: [PATCH] v1.2.1 - Feature: Automatische & Skalierbare Branchen-Regeln MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Feature: Erstellt nun automatisch kontext-sensitive Branchen-Regeln direkt in der `keyword_rules.json`. - Das Skript liest die neue Spalte 'Branche' aus dem 'CRM_Jobtitles'-Sheet. - NEU: Eine zentrale `BRANCH_GROUP_RULES` Konfiguration wurde hinzugefügt, um Branchen zu logischen Gruppen (z.B. "Bau", "Versorger") zusammenzufassen. - NEU: Das Skript analysiert die Verteilung der Jobtitel pro Department über die Branchen-Gruppen. - Wenn ein Department eine hohe Konzentration (Standard >80%) in einer Branchen-Gruppe aufweist, wird es als "branchenspezifisch" markiert. - Die relevanten Keywords der Branchen-Gruppe werden dann automatisch in den neuen `required_branch_keywords`-Eintrag in der `keyword_rules.json` geschrieben. - Entfernt die Notwendigkeit, Branchen-Regeln manuell im Code zu pflegen. --- knowledge_base_builder.py | 116 ++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/knowledge_base_builder.py b/knowledge_base_builder.py index 43cc93c2..10d27479 100644 --- a/knowledge_base_builder.py +++ b/knowledge_base_builder.py @@ -1,4 +1,6 @@ -__version__ = "v1.0.0" +# knowledge_base_builder.py + +__version__ = "v1.1.0" import logging import json @@ -6,19 +8,14 @@ import re from collections import Counter import pandas as pd -# Importiere die existierenden, robusten Handler und Konfigurationen from google_sheet_handler import GoogleSheetHandler from config import Config # --- Konfiguration --- -# Name des Tabellenblatts, das die Rohdaten der Jobtitel enthält SOURCE_SHEET_NAME = "CRM_Jobtitles" -# Namen der finalen Ausgabedateien EXACT_MATCH_OUTPUT_FILE = "exact_match_map.json" KEYWORD_RULES_OUTPUT_FILE = "keyword_rules.json" -# Priorisierung der Departments (von spezifisch zu allgemein) -# Niedrigere Zahl = höhere Priorität DEPARTMENT_PRIORITIES = { # --- Tier 1: Ultra-spezifische Nischen (höchste Priorität) --- "Fuhrparkmanagement": 1, @@ -34,10 +31,10 @@ DEPARTMENT_PRIORITIES = { "Procurement / Einkauf": 6, "Supply Chain Management": 7, "Finanzen": 8, - "Technik": 8, # Gleiche Prio wie Finanzen, da sehr ähnliche Frequenz + "Technik": 8, # --- Tier 3: Übergreifende & Allgemeine Funktionen --- - "Management / GF / C-Level": 10, # Muss niedriger als Fachbereiche sein! + "Management / GF / C-Level": 10, "Logistik": 11, "Vertrieb": 12, "Transportwesen": 13, @@ -47,7 +44,28 @@ DEPARTMENT_PRIORITIES = { "Undefined": 99 } -# Stoppwörter: Häufige Wörter in Jobtiteln, die wenig Aussagekraft für die Abteilung haben +# NEU: Definition von Branchen-Gruppen für die kontextsensitive Regelerstellung +# Key: Ein einfaches, normalisiertes Schlüsselwort für die Gruppe +# Value: Eine Liste von d365_branch_detail Werten aus Ihrer config.py +BRANCH_GROUP_RULES = { + "bau": [ + "Baustoffhandel", "Baustoffindustrie", + "Logistiker Baustoffe", "Bauunternehmen" + ], + "versorger": [ + "Stadtwerke", "Verteilnetzbetreiber", + "Telekommunikation", "Gase & Mineralöl" + ], + "produktion": [ + "Maschinenbau", "Automobil", "Anlagenbau", "Medizintechnik", + "Chemie & Pharma", "Elektrotechnik", "Lebensmittelproduktion", + "Bürotechnik", "Automaten (Vending, Slot)", "Gebäudetechnik Allgemein", + "Braune & Weiße Ware", "Fenster / Glas", "Getränke", "Möbel", "Agrar, Pellets" + ] +} +# Schwellenwert: Wenn >X% der Jobtitel eines Departments in einer Branchengruppe liegen, wird es spezifisch +BRANCH_SPECIFICITY_THRESHOLD = 0.8 + STOP_WORDS = { 'manager', 'leiter', 'head', 'lead', 'senior', 'junior', 'direktor', 'director', 'verantwortlicher', 'beauftragter', 'referent', 'sachbearbeiter', 'mitarbeiter', @@ -61,47 +79,36 @@ STOP_WORDS = { def build_knowledge_base(): """ Hauptfunktion zur Erstellung der Wissensbasis. - Liest die Rohdaten aus Google Sheets, analysiert sie und erstellt zwei JSON-Dateien. + Liest Rohdaten, analysiert sie und erstellt JSON-Dateien für exakte und Keyword-basierte Übereinstimmungen. + Erstellt automatisch Regeln für branchenspezifische Departments. """ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) - logger.info(f"Starte Erstellung der Wissensbasis (Version {__version__})...") - # 1. Daten aus Google Sheet laden - try: - gsh = GoogleSheetHandler() - df = gsh.get_sheet_as_dataframe(SOURCE_SHEET_NAME) + gsh = GoogleSheetHandler() + df = gsh.get_sheet_as_dataframe(SOURCE_SHEET_NAME) - if df is None or df.empty: - logger.critical(f"Konnte keine Daten aus '{SOURCE_SHEET_NAME}' laden oder das Tabellenblatt ist leer. Abbruch.") - return + if df is None or df.empty: + logger.critical(f"Konnte keine Daten aus '{SOURCE_SHEET_NAME}' laden oder das Tabellenblatt ist leer. Abbruch.") + return - # Spaltennamen normalisieren (z.B. Leerzeichen am Ende entfernen) - df.columns = [col.strip() for col in df.columns] - - if "Job Title" not in df.columns or "Department" not in df.columns: - logger.critical(f"Benötigte Spalten 'Job Title' und/oder 'Department' nicht in '{SOURCE_SHEET_NAME}' gefunden. Abbruch.") - return - - except Exception as e: - logger.critical(f"Ein kritischer Fehler ist beim Laden der Google Sheet Daten aufgetreten: {e}") + df.columns = [col.strip() for col in df.columns] + + required_cols = ["Job Title", "Department", "Branche"] + if not all(col in df.columns for col in required_cols): + logger.critical(f"Benötigte Spalten {required_cols} nicht in '{SOURCE_SHEET_NAME}' gefunden. Abbruch.") return - logger.info(f"{len(df)} Zeilen erfolgreich aus '{SOURCE_SHEET_NAME}' geladen.") + logger.info(f"{len(df)} Zeilen aus '{SOURCE_SHEET_NAME}' geladen.") - # 2. Daten bereinigen und vorbereiten - df.dropna(subset=["Job Title", "Department"], inplace=True) + df.dropna(subset=required_cols, inplace=True) df = df[df["Job Title"].str.strip() != ''] df['normalized_title'] = df['Job Title'].str.lower().str.strip() - logger.info(f"{len(df)} Zeilen nach Bereinigung (Entfernen leerer Jobtitel/Departments).") + logger.info(f"{len(df)} Zeilen nach Bereinigung.") - # 3. Stufe 1: "Primary Mapping" für exakte Treffer erstellen logger.info("Erstelle 'Primary Mapping' für exakte Treffer (Stufe 1)...") - # Für jeden Jobtitel, finde das am häufigsten zugewiesene Department - # .mode()[0] ist ein robuster Weg, den häufigsten Wert zu bekommen exact_match_map = df.groupby('normalized_title')['Department'].apply(lambda x: x.mode()[0]).to_dict() - try: with open(EXACT_MATCH_OUTPUT_FILE, 'w', encoding='utf-8') as f: json.dump(exact_match_map, f, indent=4, ensure_ascii=False) @@ -110,35 +117,44 @@ def build_knowledge_base(): logger.error(f"Fehler beim Schreiben der Datei '{EXACT_MATCH_OUTPUT_FILE}': {e}") return - # 4. Stufe 2: "Keyword-Datenbank" für regelbasiertes Matching erstellen - logger.info("Erstelle 'Keyword-Datenbank' mit Prioritäten (Stufe 2)...") + logger.info("Erstelle 'Keyword-Datenbank' mit automatischer Branchen-Logik (Stufe 2)...") - # Ordne jedem Department eine Liste seiner (normalisierten) Jobtitel zu titles_by_department = df.groupby('Department')['normalized_title'].apply(list).to_dict() + branches_by_department = df.groupby('Department')['Branche'].apply(list).to_dict() keyword_rules = {} for department, titles in titles_by_department.items(): all_words = [] - # Zerlege alle Jobtitel in einzelne Wörter for title in titles: - words = re.split(r'[\s/(),-]+', title) # Trennt bei Leerzeichen, /, (, ), - + words = re.split(r'[\s/(),-]+', title) all_words.extend([word for word in words if word]) - # Zähle die Worthäufigkeiten und filtere die relevantesten word_counts = Counter(all_words) - top_keywords = [] - for word, count in word_counts.most_common(50): # Nimm die 50 häufigsten als Kandidaten - # Keyword muss aussagekräftig sein - if word not in STOP_WORDS and (len(word) > 2 or word in {'it', 'edv'}): - top_keywords.append(word) + top_keywords = [word for word, count in word_counts.most_common(50) if word not in STOP_WORDS and (len(word) > 2 or word in {'it', 'edv'})] if top_keywords: - priority = DEPARTMENT_PRIORITIES.get(department, 99) # 99 als Fallback-Priorität - keyword_rules[department] = { - "priority": priority, + rule = { + "priority": DEPARTMENT_PRIORITIES.get(department, 99), "keywords": sorted(top_keywords) } - logger.debug(f" - Department '{department}' (Prio {priority}): {len(top_keywords)} Keywords gefunden (z.B. {top_keywords[:5]}).") + + department_branches = branches_by_department.get(department, []) + total_titles_in_dept = len(department_branches) + + if total_titles_in_dept > 10: # Mindestanzahl an Datenpunkten, um eine Regel zu erstellen + branch_group_counts = Counter() + for branch_name in department_branches: + for group_keyword, d365_names in BRANCH_GROUP_RULES.items(): + if branch_name in d365_names: + branch_group_counts[group_keyword] += 1 + + if branch_group_counts: + most_common_group, count = branch_group_counts.most_common(1)[0] + if (count / total_titles_in_dept) > BRANCH_SPECIFICITY_THRESHOLD: + logger.info(f" -> Department '{department}' ist spezifisch für Branche '{most_common_group}' ({count/total_titles_in_dept:.0%}). Regel wird hinzugefügt.") + rule["required_branch_keywords"] = [most_common_group] + + keyword_rules[department] = rule try: with open(KEYWORD_RULES_OUTPUT_FILE, 'w', encoding='utf-8') as f: