Files
Brancheneinstufung2/knowledge_base_builder.py
Floke 7e2f057357 v1.2.4 - Priorisierung nach Geschäfts-Relevanz & Keyword-Optimierung
- Das Priorisierungsmodell wurde grundlegend überarbeitet und orientiert sich nun an der tatsächlichen Häufigkeit und Relevanz der Departments, anstatt an semantischer Eindeutigkeit.
- Die `STOP_WORDS`-Liste wurde signifikant angepasst. Wichtige Signalwörter wie 'service', 'customer', 'care' und 'support' wurden entfernt, um die Erkennungsrate für 'Field Service Management' drastisch zu verbessern.
- Dies korrigiert systematische Fehlzuordnungen und stellt sicher, dass die generierte Wissensbasis die Geschäftsrealität korrekt abbildet.
2025-09-19 08:58:06 +00:00

195 lines
8.3 KiB
Python

# knowledge_base_builder.py
__version__ = "v1.2.4"
import logging
import json
import re
import os
import sys
from collections import Counter
import pandas as pd
from google_sheet_handler import GoogleSheetHandler
from helpers import create_log_filename
from config import Config
# --- Konfiguration ---
SOURCE_SHEET_NAME = "CRM_Jobtitles"
EXACT_MATCH_OUTPUT_FILE = "exact_match_map.json"
KEYWORD_RULES_OUTPUT_FILE = "keyword_rules.json"
# --- NEU: Priorisierung nach Geschäfts-Relevanz ---
DEPARTMENT_PRIORITIES = {
# Tier 1: Kern-Fachabteilungen (geordnet nach Häufigkeit)
"Field Service Management / Kundenservice": 1,
"IT": 2,
"Logistik": 3,
"Production Maintenance / Wartung Produktion": 4,
"Utility Maintenance": 5,
"Procurement / Einkauf": 6,
"Vertrieb": 7,
"Supply Chain Management": 8,
"Finanzen": 9,
"Technik": 10,
"Transportwesen": 11,
# Tier 2: Spezifische Nischen-Abteilungen (geordnet nach Häufigkeit)
"Fuhrparkmanagement": 15,
"Legal": 16,
"Baustofflogistik": 17,
"Baustoffherstellung": 18,
# Tier 3: Allgemeine, übergreifende Abteilungen
"Management / GF / C-Level": 20, # Muss niedriger als Fachabteilungen sein
# Tier 4: Auffang-Kategorien
"Berater": 25,
"Undefined": 99
}
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"]
}
MIN_SAMPLES_FOR_BRANCH_RULE = 5
BRANCH_SPECIFICITY_THRESHOLD = 0.6
# --- OPTIMIERTE STOP_WORDS LISTE ---
STOP_WORDS = {
# Administrative Titelteile
'manager', 'leiter', 'head', 'lead', 'senior', 'junior', 'direktor', 'director',
'verantwortlicher', 'beauftragter', 'referent', 'sachbearbeiter', 'mitarbeiter',
'spezialist', 'specialist', 'expert', 'experte', 'consultant',
'assistant', 'assistenz', 'teamleiter', 'teamlead', 'abteilungsleiter',
'bereichsleiter', 'gruppenleiter', 'geschäftsführer', 'vorstand', 'ceo', 'cio',
'cfo', 'cto', 'coo',
# Füllwörter
'von', 'of', 'und', 'für', 'der', 'die', 'das', '&',
# Zu allgemeine Begriffe, die aber Signalwörter überstimmen
'leitung', 'leiterin', 'teamleitung', 'gruppenleitung', 'bereichsleitung', 'abteilungsleitung',
'operations', 'business', 'development', 'zentrale', 'center'
# WICHTIG: 'service', 'customer', 'care', 'support' wurden bewusst entfernt!
}
def setup_logging():
log_filename = create_log_filename("knowledge_base_builder")
if not log_filename:
print("KRITISCHER FEHLER: Log-Datei konnte nicht erstellt werden. Logge nur in die Konsole.")
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()])
return
log_level = logging.DEBUG
root_logger = logging.getLogger()
if root_logger.handlers:
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_filename, encoding='utf-8'),
logging.StreamHandler()
]
)
logging.getLogger("gspread").setLevel(logging.WARNING)
logging.getLogger("oauth2client").setLevel(logging.WARNING)
logging.info(f"Logging erfolgreich initialisiert. Log-Datei: {log_filename}")
def build_knowledge_base():
logger = logging.getLogger(__name__)
logger.info(f"Starte Erstellung der Wissensbasis (Version {__version__})...")
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. Abbruch.")
return
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 aus '{SOURCE_SHEET_NAME}' geladen.")
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.")
logger.info("Erstelle 'Primary Mapping' für exakte Treffer (Stufe 1)...")
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)
logger.info(f"-> '{EXACT_MATCH_OUTPUT_FILE}' mit {len(exact_match_map)} Titeln erstellt.")
except IOError as e:
logger.error(f"Fehler beim Schreiben der Datei '{EXACT_MATCH_OUTPUT_FILE}': {e}")
return
logger.info("Erstelle 'Keyword-Datenbank' mit automatischer Branchen-Logik (Stufe 2)...")
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 = []
for title in titles:
words = re.split(r'[\s/(),-]+', title)
all_words.extend([word for word in words if word])
word_counts = Counter(all_words)
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:
rule = {
"priority": DEPARTMENT_PRIORITIES.get(department, 99),
"keywords": sorted(top_keywords)
}
department_branches = branches_by_department.get(department, [])
total_titles_in_dept = len(department_branches)
if total_titles_in_dept >= MIN_SAMPLES_FOR_BRANCH_RULE:
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]
ratio = count / total_titles_in_dept
if ratio > BRANCH_SPECIFICITY_THRESHOLD:
logger.info(f" -> Department '{department}' ist spezifisch für Branche '{most_common_group}' ({ratio:.0%}). Regel wird hinzugefügt.")
rule["required_branch_keywords"] = [most_common_group]
else:
logger.debug(f" -> Department '{department}' nicht spezifisch genug. Dominante Branche '{most_common_group}' nur bei {ratio:.0%}, benötigt >{BRANCH_SPECIFICITY_THRESHOLD:.0%}.")
else:
logger.debug(f" -> Department '{department}' konnte keiner Branchen-Gruppe zugeordnet werden.")
else:
logger.debug(f" -> Department '{department}' hat zu wenige Datenpunkte ({total_titles_in_dept} < {MIN_SAMPLES_FOR_BRANCH_RULE}) für eine Branchen-Regel.")
keyword_rules[department] = rule
try:
with open(KEYWORD_RULES_OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(keyword_rules, f, indent=4, ensure_ascii=False)
logger.info(f"-> '{KEYWORD_RULES_OUTPUT_FILE}' mit Regeln für {len(keyword_rules)} Departments erstellt.")
except IOError as e:
logger.error(f"Fehler beim Schreiben der Datei '{KEYWORD_RULES_OUTPUT_FILE}': {e}")
return
logger.info("Wissensbasis erfolgreich erstellt.")
if __name__ == "__main__":
setup_logging()
build_knowledge_base()