v1.2.1 - Feature: Automatische & Skalierbare Branchen-Regeln

- 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.
This commit is contained in:
2025-09-18 12:00:04 +00:00
parent b80a123e8a
commit b05d44af2f

View File

@@ -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: