From e92e4cc84141978e1f5839625dc51ff670ff4a39 Mon Sep 17 00:00:00 2001 From: Floke Date: Thu, 22 May 2025 18:36:45 +0000 Subject: [PATCH] =?UTF-8?q?v1.7.6:=20Spalten=20f=C3=BCr=20Parent-Account?= =?UTF-8?q?=20&=20Plausi-TS;=20detaillierte=20Header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Version auf 1.7.6 erhöht. - Einführung von drei neuen Spalten zur Handhabung von Konzernstrukturen: - "Parent Account Name" (manuell/CRM) an Position D. - "System Vorschlag Parent Account" an Position O. - "Parent Vorschlag Status" an Position P. - Einführung der Spalte "Plausibilität Prüfdatum" an Position BI für den Timestamp der Plausibilitäts-Checks. - Alle nachfolgenden Spaltenindizes in `COLUMN_MAP` entsprechend der neuen 67-Spalten-Struktur (A-BO) angepasst. - `alignment_demo`-Funktion (`new_headers`) vollständig überarbeitet, um alle 67 Spalten mit ausführlichen Beschreibungen und Aufgaben für Zeilen 4 & 5 zu reflektieren (basierend auf wiederhergestellten Originalbeschreibungen und neuen Spaltendefinitionen). - Ziel: Präzisere Datenerfassung für Konzernzugehörigkeiten und bessere Nachvollziehbarkeit der Spaltenbedeutungen als "Single Source of Truth". --- brancheneinstufung.py | 1028 ++++++++++++++++++++--------------------- 1 file changed, 490 insertions(+), 538 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index ae443be8..eb11c3a7 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn). Autor: Christian Godelmann -Version: v1.7.5 +Version: v1.7.6 Hinweis zur Struktur: Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke @@ -107,7 +107,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Neu (Empfohlen) # --- Globale Konfiguration Klasse --- class Config: """Zentrale Konfigurationseinstellungen.""" - VERSION = "v1.7.5" + VERSION = "v1.7.6" LANG = "de" # Sprache fuer Wikipedia etc. # ACHTUNG: SHEET_URL ist hier ein Platzhalter. Ersetzen Sie ihn durch Ihre tatsaechliche URL. SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" # <<< ERSETZEN SIE DIES! @@ -196,85 +196,91 @@ class Config: # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- - # Version 1.7.4 - 57 Spalten (A-BE) + # Version 1.7.6 - 57 Spalten (A-BE) COLUMN_MAP = { - # ReEval Flag & CRM-Daten (A-M) - "ReEval Flag": 0, # A - "CRM Name": 1, # B - "CRM Kurzform": 2, # C - "CRM Website": 3, # D - "CRM Ort": 4, # E - "CRM Land": 5, # F - "CRM Beschreibung": 6, # G - "CRM Branche": 7, # H (Jetzt 1:1 zum Zielschema) - "CRM Beschreibung Branche extern": 8, # I - "CRM Anzahl Techniker": 9, # J - "CRM Umsatz": 10, # K - "CRM Anzahl Mitarbeiter": 11, # L - "CRM Vorschlag Wiki URL": 12, # M + # CRM-Daten Teil 1 (A-C) + "ReEval Flag": 0, "CRM Name": 1, "CRM Kurzform": 2, + + # Parent Account Info (D, neue Spalte) + "Parent Account Name": 3, # D (NEU) - # Wikipedia-Daten & -Status (N-AB) - "Wiki URL": 13, # N - "Wiki Sitz Stadt": 14, # O - "Wiki Sitz Land": 15, # P - "Wiki Absatz": 16, # Q - "Wiki Branche": 17, # R - "Wiki Umsatz": 18, # S - "Wiki Mitarbeiter": 19, # T - "Wiki Kategorien": 20, # U - "Wikipedia Timestamp": 21, # V - "Wiki Verif. Timestamp": 22, # W - "SerpAPI Wiki Search Timestamp": 23, # X - "Chat Wiki Konsistenzpruefung": 24, # Y - "Chat Begruendung Wiki Inkonsistenz": 25, # Z - "Chat Vorschlag Wiki Artikel": 26, # AA - "Begruendung bei Abweichung": 27, # AB + # CRM-Daten Teil 2 (E-P, alte D-M verschoben um +1) + "CRM Website": 4, # E (vorher D) + "CRM Ort": 5, # F (vorher E) + "CRM Land": 6, # G (vorher F) + "CRM Beschreibung": 7, # H (vorher G) + "CRM Branche": 8, # I (vorher H) + "CRM Beschreibung Branche extern": 9, # J (vorher I) + "CRM Anzahl Techniker": 10, # K (vorher J) + "CRM Umsatz": 11, # L (vorher K) + "CRM Anzahl Mitarbeiter": 12, # M (vorher L) + "CRM Vorschlag Wiki URL": 13, # N (vorher M) - # Website-Daten (AC-AG) - "Website Rohtext": 28, # AC - "Website Zusammenfassung": 29, # AD - "Website Meta-Details": 30, # AE - "Website Scrape Timestamp": 31, # AF - "URL Prüfstatus": 32, # AG + # System Vorschlag Parent & Status (O-P, neue Spalten) + "System Vorschlag Parent Account": 14, # O (NEU) + "Parent Vorschlag Status": 15, # P (NEU) - # ChatGPT Branchen- & weitere Schätzungen (AH-AU) - VORHER AH-AS - "Chat Vorschlag Branche": 33, # AH - "Chat Branche Konfidenz": 34, # AI (NEU) - "Chat Konsistenz Branche": 35, # AJ (vorher AI) - "Chat Begruendung Abweichung Branche": 36, # AK (vorher AJ) - "Chat Pruefung FSM Relevanz": 37, # AL (vorher AK) - "Chat Begruendung für FSM Relevanz": 38, # AM (vorher AL) - "Chat Schaetzung Anzahl Mitarbeiter": 39, # AN (vorher AM) - "Chat Konsistenzprüfung Mitarbeiterzahl": 40, # AO (vorher AN) - "Chat Begruendung Abweichung Mitarbeiterzahl": 41, # AP (vorher AO) - "Chat Einschätzung Anzahl Servicetechniker": 42, # AQ (vorher AP) - "Chat Begruendung Abweichung Anzahl Servicetechniker": 43, # AR (vorher AQ) - "Chat Schätzung Umsatz": 44, # AS (vorher AR) - "Chat Begruendung Abweichung Umsatz": 45, # AT (vorher AS) - # Spalte AU ist jetzt leer, war vorher "Linked Serviceleiter gefunden" + # Wikipedia-Daten & -Status (Q-AF, alte N-AB verschoben um +2) + "Wiki URL": 16, # Q (vorher N) + "Wiki Sitz Stadt": 17, # R (vorher O) + "Wiki Sitz Land": 18, # S (vorher P) + "Wiki Absatz": 19, # T (vorher Q) + "Wiki Branche": 20, # U (vorher R) + "Wiki Umsatz": 21, # V (vorher S) + "Wiki Mitarbeiter": 22, # W (vorher T) + "Wiki Kategorien": 23, # X (vorher U) + "Wikipedia Timestamp": 24, # Y (vorher V) + "Wiki Verif. Timestamp": 25, # Z (vorher W) + "SerpAPI Wiki Search Timestamp": 26, # AA (vorher X) + "Chat Wiki Konsistenzpruefung": 27, # AB (vorher Y) + "Chat Begruendung Wiki Inkonsistenz": 28, # AC (vorher Z) + "Chat Vorschlag Wiki Artikel": 29, # AD (vorher AA) + "Begruendung bei Abweichung": 30, # AE (vorher AB) - # LinkedIn-Kontakte (AU-AX) - VORHER AT-AW - "Linked Serviceleiter gefunden": 46, # AU (vorher AT) - "Linked It-Leiter gefunden": 47, # AV (vorher AU) - "Linked Management gefunden": 48, # AW (vorher AV) - "Linked Disponent gefunden": 49, # AX (vorher AW) + # Website-Daten (AG-AK, alte AC-AG verschoben um +3) + "Website Rohtext": 31, # AF (vorher AC) + "Website Zusammenfassung": 32, # AG (vorher AD) + "Website Meta-Details": 33, # AH (vorher AE) + "Website Scrape Timestamp": 34, # AI (vorher AF) + "URL Prüfstatus": 35, # AJ (vorher AG) - "Contact Search Timestamp": 50, # AY (KORREKT VERSCHOBEN) - "Finaler Umsatz (Wiki>CRM)": 51, # AZ (war AY) - "Finaler Mitarbeiter (Wiki>CRM)": 52, # BA (war AZ) - "Geschaetzter Techniker Bucket": 53, # BB (war BA) - "Plausibilität Umsatz": 54, # BC - "Plausibilität Mitarbeiter": 55, # BD - "Plausibilität Umsatz/MA Ratio": 56, # BE - "Abweichung Umsatz CRM/Wiki": 57, # BF - "Abweichung MA CRM/Wiki": 58, # BG - "Plausibilität Begründung": 59, # BH - "Plausibilität Prüfdatum": 60, # BI + # ChatGPT Branchen- & weitere Schätzungen (AL-AX, alte AH-AT verschoben um +3) + "Chat Vorschlag Branche": 36, # AK (vorher AH) + "Chat Branche Konfidenz": 37, # AL (vorher AI) + "Chat Konsistenz Branche": 38, # AM (vorher AJ) + "Chat Begruendung Abweichung Branche": 39, # AN (vorher AK) + "Chat Pruefung FSM Relevanz": 40, # AO (vorher AL) + "Chat Begruendung für FSM Relevanz": 41, # AP (vorher AM) + "Chat Schaetzung Anzahl Mitarbeiter": 42, # AQ (vorher AN) + "Chat Konsistenzprüfung Mitarbeiterzahl": 43, # AR (vorher AO) + "Chat Begruendung Abweichung Mitarbeiterzahl": 44, # AS (vorher AP) + "Chat Einschätzung Anzahl Servicetechniker": 45, # AT (vorher AQ) + "Chat Begruendung Abweichung Anzahl Servicetechniker": 46, # AU (vorher AR) + "Chat Schätzung Umsatz": 47, # AV (vorher AS) + "Chat Begruendung Abweichung Umsatz": 48, # AW (vorher AT) + + # LinkedIn-Kontakte (AY-BB, alte AU-AX verschoben um +3) + "Linked Serviceleiter gefunden": 49, # AX (vorher AU) + "Linked It-Leiter gefunden": 50, # AY (vorher AV) + "Linked Management gefunden": 51, # AZ (vorher AW) + "Linked Disponent gefunden": 52, # BA (vorher AX) - # Systemspalten (BJ-BL) - "Timestamp letzte Pruefung": 61, # BJ (ChatGPT Eval TS) - "Version": 62, # BK - "Tokens": 63, # BL + # Konsolidierte Werte, Timestamps, Plausi & System (BC-BO) + # Reihenfolge von "Contact Search Timestamp" nach Ihren letzten Wünschen + "Contact Search Timestamp": 53, # BB (war AY in der 64-Spalten-Version) + "Finaler Umsatz (Wiki>CRM)": 54, # BC (war AZ in der 64-Spalten-Version) + "Finaler Mitarbeiter (Wiki>CRM)": 55, # BD (war BA in der 64-Spalten-Version) + "Geschaetzter Techniker Bucket": 56, # BE (war BB in der 64-Spalten-Version) + "Plausibilität Umsatz": 57, # BF (war BC in der 64-Spalten-Version) + "Plausibilität Mitarbeiter": 58, # BG (war BD in der 64-Spalten-Version) + "Plausibilität Umsatz/MA Ratio": 59, # BH (war BE in der 64-Spalten-Version) + "Abweichung Umsatz CRM/Wiki": 60, # BI (war BF in der 64-Spalten-Version) + "Abweichung MA CRM/Wiki": 61, # BJ (war BG in der 64-Spalten-Version) + "Plausibilität Begründung": 62, # BK (war BH in der 64-Spalten-Version) + "Plausibilität Prüfdatum": 63, # BL (war BI in der 64-Spalten-Version) + "Timestamp letzte Pruefung": 64, # BM (ChatGPT Eval TS, war BJ) + "Version": 65, # BN (war BK) + "Tokens": 66, # BO (war BL) } # --- Globale Variablen fuer Branch Mapping (werden von load_target_schema() befuellt) --- @@ -801,88 +807,78 @@ def extract_numeric_value(raw_value, is_umsatz=False): # Basierend auf Code aus Teil 2. # Extrahiert und normalisiert Zahlenwerte fuer Vergleichslogik. # Nutzt globale Helfer: clean_text, re. +# Globale Funktion (ersetzen Sie Ihre bestehende Version) def get_numeric_filter_value(value_str, is_umsatz=False): logger = logging.getLogger(__name__) if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': return 0.0 if is_umsatz else 0 - raw_value_str_original = str(value_str).strip() # Original für Einheiten-Keywords behalten + raw_value_str_original = str(value_str).strip() - # "0" und "k.A." als expliziter String wird für Filterzwecke wie 0 behandelt - if raw_value_str_original.lower() in ['k.a.', 'n/a', '-', '0', '0.0', '0,00', '0,000']: + if raw_value_str_original.lower() in ['k.a.', 'n/a', '-']: + return 0.0 if is_umsatz else 0 + + # "0" als expliziter String wird hier für Filterzwecke wie 0 behandelt. + # Die Plausi-Logik wird "0" (falls es in die finale Spalte geschrieben wird) + # dann über _get_numeric_value_for_plausi als "unbekannt" interpretieren. + if raw_value_str_original in ['0', '0.0', '0,0', '0.00', '0,000', '0.000']: return 0.0 if is_umsatz else 0 try: - # Bereinigung für die Zahlenextraktion - processed_value = clean_text(raw_value_str_original) # clean_text ist Ihre globale Funktion + processed_value = clean_text(raw_value_str_original) if processed_value.lower() in ['k.a.', 'n/a', '-']: return 0.0 if is_umsatz else 0 - # Präfixe und Währungssymbole entfernen processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|ueber|unter|mehr als|weniger als|bis zu)\s+', '', processed_value) processed_value = re.sub(r'[€$£¥]', '', processed_value).strip() - # Nur den ersten Teil bei Spannen nehmen (z.B. "100 - 200" -> "100") processed_value = re.split(r'\s*(-|–|bis)\s*', processed_value, 1)[0].strip() - # Umgang mit Tausendertrennern (Punkt) und Dezimalkomma - # Ziel: Einen String erhalten, der von float() korrekt als Zahl interpretiert wird (Dezimaltrenner ist Punkt) - - num_extraction_str = processed_value.replace("'", "") # Apostroph als Tausendertrenner entfernen + # Tausendertrenner entfernen (Apostroph, Punkt wenn nicht Dezimal) + # Komma zu Punkt für Dezimal machen + num_extraction_str = processed_value.replace("'", "") # Apostrophe immer entfernen - # Wenn Punkt und Komma vorkommen -> versuche Format zu erkennen - if '.' in num_extraction_str and ',' in num_extraction_str: - if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US-Format: 1,234.56 -> Kommas entfernen + if '.' in num_extraction_str and ',' in num_extraction_str: # Enthält Punkt und Komma + if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US-Stil: 1,234.56 -> Kommas entfernen num_extraction_str = num_extraction_str.replace(',', '') - else: # EU-Format: 1.234,56 -> Punkte entfernen, Komma zu Punkt + else: # EU-Stil: 1.234,56 -> Punkte entfernen, Komma zu Punkt num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') - elif ',' in num_extraction_str: # Nur Kommas: 1234,56 -> 1234.56 + elif ',' in num_extraction_str: # Nur Kommas als Dezimaltrenner: 1234,56 -> 1234.56 num_extraction_str = num_extraction_str.replace(',', '.') elif '.' in num_extraction_str: # Nur Punkte - # Wenn der String ein typisches Tausenderformat hat (z.B. 1.234 oder 1.234.567), Punkte entfernen - if re.fullmatch(r'\d{1,3}(\.\d{3})+', num_extraction_str): # z.B. 4.380 oder 17.800 + # Wenn der String klar ein Tausenderformat hat (z.B. 1.234 oder 1.234.567), Punkte entfernen + if re.fullmatch(r'^\d{1,3}(\.\d{3})+$', num_extraction_str): num_extraction_str = num_extraction_str.replace('.', '') - # Ansonsten (z.B. "123.45") ist der Punkt wahrscheinlich ein Dezimalpunkt und bleibt. - # Wenn mehrere Punkte und es ist kein Tausenderformat (z.B. 1.2.3), wird es problematisch - # Hier ist eine einfache Annahme: Wenn mehr als ein Punkt und kein Tausenderformat, ist es ungültig. - elif num_extraction_str.count('.') > 1: - logger.debug(f"get_numeric_filter_value: Mehrere Punkte in '{num_extraction_str}', die nicht als Tausendertrenner erkannt wurden. Interpretiere als ungültig.") + # Ansonsten ist ein einzelner Punkt ein Dezimalpunkt (z.B. "123.45") oder es ist ungültig + elif num_extraction_str.count('.') > 1: # Mehrere Punkte nicht im Tausenderformat sind ungültig + logger.debug(f"get_numeric_filter_value: Mehrere Punkte in '{num_extraction_str}' (nicht als Tausenderformat erkannt).") return 0.0 if is_umsatz else 0 - - + # Extrahiere die Zahl (kann jetzt einen Dezimalpunkt enthalten) - match = re.search(r'([\d.\-]+)', num_extraction_str) # Erlaube Dezimalpunkt und optionales Minus + match = re.search(r'([\d.\-]+)', num_extraction_str) if not match: - # logger.debug(f"get_numeric_filter_value: Kein numerischer Match in '{num_extraction_str}' (aus '{raw_value_str_original}')") return 0.0 if is_umsatz else 0 num_str_for_float = match.group(1) # Finale Prüfung des extrahierten Zahlenstrings - if not num_str_for_float or \ - num_str_for_float == '.' or \ - num_str_for_float == '-' or \ + if not num_str_for_float or num_str_for_float == '.' or num_str_for_float == '-' or \ (num_str_for_float.count('.') > 1) or \ (num_str_for_float.count('-') > 1) or \ (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): - # logger.debug(f"get_numeric_filter_value: Ungültiger Zahlenstring '{num_str_for_float}' nach Regex.") return 0.0 if is_umsatz else 0 num_as_float = float(num_str_for_float) - # Einheiten-Skalierung basierend auf dem ORIGINALSTRING raw_value_str_original - # Annahme: Wenn is_umsatz=True, ist num_as_float bereits in Millionen, es sei denn, - # es gibt explizite Einheiten wie "Mrd" oder "Tsd" im Originalstring. - scaled_num = num_as_float original_lower = raw_value_str_original.lower() - if is_umsatz: + if is_umsatz: # Wert soll in Mio. € sein if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - scaled_num = num_as_float * 1000.0 # Der num_as_float war die Zahl vor Mrd, jetzt ist es Mio. + scaled_num = num_as_float * 1000.0 # von Mrd-Einheit zu Mio-Einheit elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - scaled_num = num_as_float / 1000.0 # Der num_as_float war die Zahl vor Tsd, jetzt ist es Mio. - # Ansonsten ist num_as_float bereits der Wert in Millionen - else: # Mitarbeiter (absolute Zahl erwartet, außer bei expliziten Einheiten) + scaled_num = num_as_float / 1000.0 # von Tsd-Einheit zu Mio-Einheit + # Ansonsten wird angenommen, dass num_as_float bereits den Wert in Mio. € darstellt + else: # Mitarbeiter (absolute Zahl) if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): scaled_num = num_as_float * 1000000000.0 elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): @@ -890,13 +886,12 @@ def get_numeric_filter_value(value_str, is_umsatz=False): elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): scaled_num = num_as_float * 1000.0 - # Für die Filterlogik nur positive Werte zurückgeben, sonst 0 return scaled_num if scaled_num > 0 else (0.0 if is_umsatz else 0) - except ValueError as e: # Fehler bei float() Konvertierung + except ValueError as e: logger.debug(f"get_numeric_filter_value: ValueError '{e}' bei Konvertierung von '{num_str_for_float if 'num_str_for_float' in locals() else raw_value_str_original[:30]}...'") return 0.0 if is_umsatz else 0 - except Exception as e_general: # Andere unerwartete Fehler + except Exception as e_general: logger.error(f"Unerwarteter Fehler in get_numeric_filter_value für '{raw_value_str_original[:50]}...': {e_general}") logger.debug(traceback.format_exc()) return 0.0 if is_umsatz else 0 @@ -2523,185 +2518,193 @@ def alignment_demo(sheet): logger.info("Starte Alignment Demo für das Hauptblatt...") new_headers = [ - [ # Zeile 1: Spaltenname - # ... (A-AX unverändert lassen bis "Linked Disponent gefunden") ... - "ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Land", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", - "Wiki URL", "Wiki Sitz Stadt", "Wiki Sitz Land", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Wikipedia Timestamp", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp", "Chat Wiki Konsistenzpruefung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", + [ # Zeile 1: Spaltenname (67 Spalten) + "ReEval Flag", "CRM Name", "CRM Kurzform", + "Parent Account Name", "CRM Website", "CRM Ort", "CRM Land", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", + "System Vorschlag Parent Account", "Parent Vorschlag Status", + "Wiki URL", "Wiki Sitz Stadt", "Wiki Sitz Land", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Wikipedia Timestamp", "Wiki Verif. Timestamp", "SerpAPI Wiki Search Timestamp", "Chat Wiki Konsistenzpruefung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Website Rohtext", "Website Zusammenfassung", "Website Meta-Details", "Website Scrape Timestamp", "URL Prüfstatus", "Chat Vorschlag Branche", "Chat Branche Konfidenz", "Chat Konsistenz Branche", "Chat Begruendung Abweichung Branche", "Chat Prüfung FSM Relevanz", "Chat Begründung für FSM Relevanz", "Chat Schätzung Anzahl Mitarbeiter", "Chat Konsistenzprüfung Mitarbeiterzahl", "Chat Begründung Abweichung Mitarbeiterzahl", "Chat Einschätzung Anzahl Servicetechniker", "Chat Begründung Abweichung Anzahl Servicetechniker", "Chat Schätzung Umsatz", "Chat Begründung Abweichung Umsatz", "Linked Serviceleiter gefunden", "Linked It-Leiter gefunden", "Linked Management gefunden", "Linked Disponent gefunden", - # --- KORRIGIERTE REIHENFOLGE / NEUE SPALTEN AB AY --- "Contact Search Timestamp", "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", "Geschaetzter Techniker Bucket", "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", "Plausibilität Prüfdatum", "Timestamp letzte Prüfung", "Version", "Tokens" ], - [ # Zeile 2: Quelle der Daten - "CRM", "CRM", "CRM", "CRM", "CRM", "CRM/Manuell", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", - "Wikipediascraper/SerpAPI/ChatGPT/Manuell", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "System", "System", "System", "ChatGPT API", "ChatGPT API", "ChatGPT API", "System/Manuell", - "Web Scraper", "ChatGPT API", "Web Scraper", "System", "System/Web Scraper", - "ChatGPT API", "ChatGPT API", "System/ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "System/ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", - "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", - "System", # AY: Contact Search Timestamp - "Skript (Wiki/CRM Logik)", # AZ: Finaler Umsatz - "Skript (Wiki/CRM Logik)", # BA: Finaler Mitarbeiter - "ML Modell / Skript", # BB: Geschaetzter Techniker Bucket - "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", # BC-BH: Plausi - "System (Plausi-Check TS)",# BI: Plausi Prüfdatum - "System", "System", "System" # BJ-BL: Rest + [ # Zeile 2: Quelle der Daten (67 Spalten) + "CRM", "CRM", "CRM", + "CRM/Manuell", # D: Parent Account Name + "CRM", "CRM", "CRM/Manuell", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", # E-N (alt D-M) + "System", "Manuell/System", # O-P: System Vorschlag Parent, Status + "Wikipediascraper/SerpAPI/ChatGPT/Manuell", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "System", "System", "System", "ChatGPT API", "ChatGPT API", "ChatGPT API", "System/Manuell", # Q-AE + "Web Scraper", "ChatGPT API", "Web Scraper", "System", "System/Web Scraper", # AF-AJ + "ChatGPT API", "ChatGPT API", "System/ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "System/ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", "ChatGPT API", # AK-AW + "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", # AX-BA + "System", # BB: Contact Search Timestamp + "Skript (Wiki/CRM Logik)", "Skript (Wiki/CRM Logik)", "ML Modell / Skript", # BC-BE + "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", "Skript (Plausi-Check)", # BF-BK + "System (Plausi-Check TS)", # BL + "System", "System", "System" # BM-BO ], - [ # Zeile 3: Feldkategorie - "Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Land", "Beschreibung (Text)", "Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", - "Wikipedia Artikel URL", "Ort", "Land", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Timestamp", "Timestamp", "Timestamp", "Verifizierung Wiki-Artikel", "Begründung Verifizierung", "Wikipedia Artikel URL (Vorschlag)", "Begründung URL-Abweichung", - "Website-Content", "Website-Content (Zusammenfassung)", "Website-Content (Meta)", "Timestamp", "Prozess-Status", - "Branche (Vorschlag KI)", "Branche (Konfidenz KI)", "Branche (Konsistenz)", "Branche (Begründung KI)", "FSM Relevanz (KI)", "FSM Relevanz (Begründung KI)", "Anzahl Mitarbeiter (KI)", "Anzahl Mitarbeiter (Konsistenz KI)", "Anzahl Mitarbeiter (Begründung KI)", "Anzahl Servicetechniker (KI)", "Anzahl Servicetechniker (Begründung KI)", "Umsatz (KI)", "Umsatz (Begründung KI)", - "Kontakte (Anzahl)", "Kontakte (Anzahl)", "Kontakte (Anzahl)", "Kontakte (Anzahl)", - "Timestamp", # AY: Contact Search Timestamp - "Umsatz (Konsolidiert)", # AZ - "Anzahl Mitarbeiter (Konsolidiert)", # BA - "Anzahl Servicetechniker (Bucket ML)", # BB - "Plausibilität", "Plausibilität", "Plausibilität", "Datenqualitäts-Indikator", "Datenqualitäts-Indikator", "Plausibilität (Text)", # BC-BH: Plausi - "Timestamp (Plausi)", # BI: Plausi Prüfdatum - "Timestamp", "Skript Version", "API Tokens" + [ # Zeile 3: Feldkategorie (67 Spalten) + "Prozess", "Firmenname", "Firmenname", + "Konzernstruktur", # D + "Website", "Ort", "Land", "Beschreibung (Text)", "Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", # E-N + "Konzernstruktur (Vorschlag)", "Konzernstruktur (Status)", # O-P + "Wikipedia Artikel URL", "Ort", "Land", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Timestamp", "Timestamp", "Timestamp", "Verifizierung Wiki-Artikel", "Begründung Verifizierung", "Wikipedia Artikel URL (Vorschlag)", "Begründung URL-Abweichung", # Q-AE + "Website-Content", "Website-Content (Zusammenfassung)", "Website-Content (Meta)", "Timestamp", "Prozess-Status", # AF-AJ + "Branche (Vorschlag KI)", "Branche (Konfidenz KI)", "Branche (Konsistenz)", "Branche (Begründung KI)", "FSM Relevanz (KI)", "FSM Relevanz (Begründung KI)", "Anzahl Mitarbeiter (KI)", "Anzahl Mitarbeiter (Konsistenz KI)", "Anzahl Mitarbeiter (Begründung KI)", "Anzahl Servicetechniker (KI)", "Anzahl Servicetechniker (Begründung KI)", "Umsatz (KI)", "Umsatz (Begründung KI)", # AK-AW + "Kontakte (Anzahl)", "Kontakte (Anzahl)", "Kontakte (Anzahl)", "Kontakte (Anzahl)", # AX-BA + "Timestamp", # BB + "Umsatz (Konsolidiert)", "Anzahl Mitarbeiter (Konsolidiert)", "Anzahl Servicetechniker (Bucket ML)", # BC-BE + "Plausibilität", "Plausibilität", "Plausibilität", "Datenqualitäts-Indikator", "Datenqualitäts-Indikator", "Plausibilität (Text)", # BF-BK + "Timestamp (Plausi)", # BL + "Timestamp", "Skript Version", "API Tokens" # BM-BO ], - [ # Zeile 4: Kurze Beschreibung - "Systemspalte, irrelevant für den Prompt. 'x' markiert Zeile für Re-Evaluation.", #A - "Enthält den Firmennamen nach bestem Gewissen...", #B - "Enthält eine manuell gepflegte (normalisierte) Kurzform des Firmennamens...", #C - "Von uns ermittelte Website des Unternehmens. Kann durch 'website_lookup' oder 'check_urls' aktualisiert werden.", #D - "Von uns ermittelter Ort des Unternehmens.", #E - "Land des Unternehmenssitzes laut CRM (manuell zu pflegen).", #F - "Kurze Beschreibung des Unternehmens aus CRM.", #G - "Aktuelle Branchenzuweisung entsprechend unserem Ziel-Branchenschema (manuell oder aus Altsystem).", #H - "Von Dealfront gelieferte externe Beschreibung der Branche...", #I - "Von uns Recherchierte Anzahl der Servicetechniker...", #J - "Von uns recherchierter Umsatz in Mio. € (aus CRM).", #K - "Von uns recherchierte Anzahl der Mitarbeiter (aus CRM).", #L - "Enthält aus einer alten Recherche Vorschläge für die Wikipedia URL zum Unternehmen...", #M - "Finale Wikipedia URL des Unternehmens.", #N - "Aus Wikipedia-Infobox extrahierte Stadt des Unternehmenssitzes.", #O - "Aus Wikipedia-Infobox extrahiertes Land des Unternehmenssitzes.", #P - "Erster aussagekräftiger Absatz des Wikipedia-Artikels (N).", #Q - "Aus der Wikipedia-Infobox extrahierte Branche(n).", #R - "Aus der Wikipedia-Infobox extrahierter Umsatz (normalisiert in Mio. €).", #S - "Aus der Wikipedia-Infobox extrahierte Mitarbeiterzahl (normalisiert).", #T - "Komma-separierte Liste der Kategorien, denen der Artikel in Wikipedia zugewiesen wurde...", #U - "Zeitstempel der letzten Wikipedia-Suche und Datenextraktion für diese Zeile.", #V - "Zeitstempel der letzten Wikipedia-Artikel-Verifizierung durch ChatGPT (Spalten Y-AA).", #W - "Zeitstempel des letzten Versuchs, eine fehlende Wiki-URL (N) über SerpAPI zu suchen.", #X - "Ergebnis der ChatGPT-Prüfung, ob der Wiki-Artikel (N) zum Unternehmen passt. ('OK', 'X', '?', 'FEHLER', etc.)", #Y - "Von ChatGPT generierte Begründung, falls der Wiki-Artikel (N) als inkonsistent zum Unternehmen bewertet wurde.", #Z - "Von ChatGPT vorgeschlagene alternative Wikipedia-URL, falls der Artikel in N als unpassend bewertet wurde.", #AA - "Wird derzeit primär geleert. Ursprünglich für Begründung bei Abweichung CRM Vorschlag Wiki URL (M alt) vs. gefundener Wiki-URL (N).", #AB - "Roh extrahierter Textinhalt der Website. Basis für Zusammenfassung. Kann auch Fehlermeldungen oder Marker enthalten.", #AC - "KI-generierte Zusammenfassung des Website-Rohtextes (AC).", #AD - "Extrahierte Meta-Daten der Website: Title, Meta-Description, H1-H3 Überschriften.", #AE - "Zeitstempel des letzten Website-Scraping/Summarization-Versuchs (für AC, AD).", #AF - "Status der URL-Prüfung (z.B. 'URL_CHECK_NEEDED', 'URL_OK').", #AG - "Branchenvorschlag von ChatGPT basierend auf allen verfügbaren Informationen.", #AH - "Konfidenz des ChatGPT-Branchenvorschlags (AH), z.B. Hoch/Mittel/Niedrig.", #AI (NEU) - "Konsistenzprüfung: 'ok', wenn ChatGPT-Vorschlag (AH) mit CRM-Branche (H) übereinstimmt.", #AJ - "Von ChatGPT generierte Begründung für den Branchenvorschlag (AH).", #AK - "Bewertung der FSM-Relevanz durch ChatGPT. (Platzhalter)", #AL - "Von ChatGPT generierte Begründung für die Einschätzung der FSM-Relevanz. (Platzhalter)", #AM - "Von ChatGPT geschätzte Anzahl der Mitarbeiter. (Platzhalter)", #AN - "Konsistenzprüfung der von ChatGPT geschätzten Mitarbeiterzahl (AM). (Platzhalter)", #AO - "Von ChatGPT generierte Begründung bei signifikanter Abweichung der geschätzten Mitarbeiterzahl. (Platzhalter)", #AP - "Von ChatGPT geschätzte Anzahl der Servicetechniker. (Platzhalter)", #AQ - "Von ChatGPT generierte Begründung für die Einschätzung der Servicetechniker-Anzahl. (Platzhalter)", #AR - "Von ChatGPT geschätzter Umsatz. (Platzhalter)", #AS - "Von ChatGPT generierte Begründung bei signifikanter Abweichung der geschätzten Umsatzzahlen. (Platzhalter)", #AT - "Anzahl der via SerpAPI gefundenen LinkedIn-Kontakte für die Kategorie 'Serviceleiter'.", #AU - "Anzahl der via SerpAPI gefundenen LinkedIn-Kontakte für die Kategorie 'IT-Leiter'.", #AV - "Anzahl der via SerpAPI gefundenen LinkedIn-Kontakte für die Kategorie 'Management'.", #AW - "Anzahl der via SerpAPI gefundenen LinkedIn-Kontakte für die Kategorie 'Disponent'.", #AX - "Zeitstempel der letzten Kontaktsuche via SerpAPI für diese Zeile.", #AY - "Konsolidierter Umsatzwert in Mio. €. Priorisiert Wiki (S) > CRM (K).", #AZ - "Konsolidierte Mitarbeiterzahl. Priorisiert Wiki (T) > CRM (L).", #BA - "Ergebnis der Schätzung durch das trainierte Machine-Learning-Modell.", #BB - "Plausibilitätsstatus für den finalen Umsatzwert (z.B. OK, WARNUNG_HOCH).", #BC - "Plausibilitätsstatus für die finale Mitarbeiterzahl (z.B. OK, WARNUNG_NIEDRIG).", #BD - "Plausibilitätsstatus für die Umsatz-pro-Mitarbeiter-Ratio.", #BE - "Indikator für Abweichung zwischen CRM- und Wiki-Umsatz.", #BF - "Indikator für Abweichung zwischen CRM- und Wiki-Mitarbeiterzahl.", #BG - "Gesammelte Begründungen für Plausibilitätswarnungen oder -fehler.", #BH - "Zeitstempel des letzten Plausibilitäts-Checks.", #BI - "Zeitstempel der letzten übergreifenden Prüfung/Bewertung durch ChatGPT (z.B. Branchenevaluation AH-AT).", #BJ - "Version des Skripts, die diese Zeile zuletzt signifikant bearbeitet hat.", #BK - "Verbrauchte Tokens für OpenAI API-Aufrufe für diese Zeile." #BL + [ # Zeile 4: Kurze Beschreibung (67 Spalten - JETZT AUSFÜHRLICH) + "Systemspalte, irrelevant für den Prompt. Wird genutzt um die manuelle Neuprüfung dieses Accounts durchzuführen.", #A ReEval Flag + "Enthält den Firmennamen nach bestem Gewissen. Firmennamen sind manchmal herausfordernd, insbesondere was unterschiedliche Schreibweisen, Firmierung, Tochter/Mutterfirmen etc. anbelangt. Zur besseren Trefferquote in der Wikipedia-Suche normalisieren wir den Firmennamen und entfernen sämtliche Firmenformen, wie z.B. AG, GmbH, SE etc.", #B CRM Name + "Enthält eine manuell gepflegte (normalisierte) Kurzform des Firmennamens, wie auch ein Mensch die Firma nennen würde. Dies bedeutet insbesondere, dass die Firmenform wie z.B. GmbH oder AG aus dem Namen entfernt wird. Meist entspricht die Kurzform den ersten beiden Worten des Firmennamens. Manchmal sind auch Worte nötig, wenn die ersten beiden worte zu wenig Aussagekraft haben. Beispiele dafür sind beispielsweise Firmen wie 'Schmidt & Söhne', bei denen 'Schmidt &' wenig Sinn machen würde, oder 'Philip Morris Tabakwaren' - weil in diesem Fall 'Philip Morris' zu generisch wäre bzw. wenig eindeutig.", #C CRM Kurzform + "Name der direkten Muttergesellschaft / des Hauptkonzerns (falls zutreffend). Manuell gepflegt oder aus CRM. Beeinflusst Konsolidierung und Plausi-Checks.", #D Parent Account Name (NEU) + "Von uns ermittelte Website des Unternehmens, sofern verfügbar. Kann durch 'website_lookup' oder 'check_urls' aktualisiert werden.", #E CRM Website + "von uns ermittelter Ort des Unternehmens", #F CRM Ort + "Land des Unternehmenssitzes laut CRM oder manueller Recherche. Wichtig für regionale Analysen (z.B. DACH).", #G CRM Land + "Kurze Beschreibung der Haupttätigkeit des Unternehmens aus dem CRM-System. Dient als Input für KI-Analysen.", #H CRM Beschreibung + "Branchenzuweisung aus dem CRM-System. Entspricht idealerweise einer Branche aus dem Ziel-Branchenschema.", #I CRM Branche + "Von externen Datenanbietern (z.B. Dealfront) gelieferte Beschreibung der Branche des Unternehmens. Diese Branchenbeschreibung sollte in den allermeisten Fällen sehr zutreffend sein und ist vermutlich verlässlicher als die aktuelle Branche aus Spalte I.", #J CRM Beschreibung Branche extern + "Bekannte Anzahl der Servicetechniker des Unternehmens (aus CRM oder Recherche). Dient als Ground Truth für ML.", #K CRM Anzahl Techniker + "Umsatz des Unternehmens in Millionen Euro laut CRM oder Recherche.", #L CRM Umsatz + "Anzahl der Mitarbeiter des Unternehmens laut CRM oder Recherche.", #M CRM Anzahl Mitarbeiter + "Enthält aus einer alten Recherche Vorschläge für die Wikipedia URL zum Unternehmen. Dieser muss aber nicht stimmen. Sollte als Ausgangs- und Vergleichspunkt für die nachgelagerte Wikipedia-Suche dienen.", #N CRM Vorschlag Wiki URL + "Vom System heuristisch ermittelter Vorschlag für den Parent Account (basierend auf Namensähnlichkeiten, Wiki-Infos etc.).", #O System Vorschlag Parent Account (NEU) + "Status des System-Vorschlags für Parent Account (z.B. 'x' für akzeptiert, '-' für abgelehnt, '?' für unklar zur manuellen Prüfung).", #P Parent Vorschlag Status (NEU) + "Finale Wikipedia URL des Unternehmens. Quelle: Wikipedia-Scraper (Suche/Extraktion), SerpAPI (Neusuche), ChatGPT (Vorschlag AD nach Prüfung), oder manuelle Eingabe.", #Q Wiki URL + "Aus Wikipedia-Infobox extrahierte Stadt des Unternehmenssitzes.", #R Wiki Sitz Stadt + "Aus Wikipedia-Infobox extrahiertes Land des Unternehmenssitzes.", #S Wiki Sitz Land + "Erster aussagekräftiger Absatz des Wikipedia-Artikels (Q).", #T Wiki Absatz + "Aus der Wikipedia-Infobox extrahierte Branche(n).", #U Wiki Branche + "Aus der Wikipedia-Infobox extrahierter Umsatz (normalisiert in Mio. €).", #V Wiki Umsatz + "Aus der Wikipedia-Infobox extrahierte Mitarbeiterzahl (normalisiert).", #W Wiki Mitarbeiter + "Komma-separierte Liste der Kategorien, denen der Artikel in Wikipedia zugewiesen wurde. Hier ist auch häufig eine Branche enthalten, häufig auch noch weitere Informationen etwa zur Gründung, ob sie etwa im DAX gelistet ist etc. Guter Anhaltspunkt zur Differenzierung von Unternehmenseinträgen und Wikipedia-Seiten, die kein Unternehmen beschreiben und fälschlicherweise zugewiesen wurden. Bei jeder Unternehmensseite MUSS das Wort unternehmen in irgendeiner Art und Weise vorkommen. NEGATIVSIGNAL: EHEMALIGES UNTERNEHMEN -> Weist darauf hin, dass das Unternehmen nicht mehr besteht.", #X Wiki Kategorien + "Zeitstempel der letzten Wikipedia-Suche und Datenextraktion für diese Zeile (für Spalten Q-X).", #Y Wikipedia Timestamp + "Zeitstempel der letzten Wikipedia-Artikel-Verifizierung durch ChatGPT (Ergebnis in Spalten AB-AD).", #Z Wiki Verif. Timestamp + "Zeitstempel des letzten Versuchs, eine fehlende Wiki-URL (Q) über SerpAPI zu suchen.", #AA SerpAPI Wiki Search Timestamp + "Ergebnis der ChatGPT-Prüfung, ob der Wiki-Artikel (Q) zum Unternehmen passt. ('OK', 'X', '?', 'FEHLER', etc.)", #AB Chat Wiki Konsistenzpruefung + "Von ChatGPT generierte Begründung, falls der Wiki-Artikel (Q) als inkonsistent zum Unternehmen bewertet wurde.", #AC Chat Begründung Wiki Inkonsistenz + "Von ChatGPT vorgeschlagene alternative Wikipedia-URL, falls der Artikel in Q als unpassend bewertet wurde.", #AD Chat Vorschlag Wiki Artikel + "Wird derzeit primär geleert. Ursprünglich für Begründung bei Abweichung CRM Vorschlag Wiki URL (N alt) vs. gefundener Wiki-URL (Q).", #AE Begründung bei Abweichung + "Roh extrahierter Textinhalt der Website. Basis für Zusammenfassung. Kann auch Fehlermeldungen oder Marker wie 'URL_CHECK_NEEDED' enthalten.", #AF Website Rohtext + "KI-generierte Zusammenfassung des Website-Rohtextes (AF).", #AG Website Zusammenfassung + "Extrahierte Meta-Daten der Website: Title, Meta-Description, H1-H3 Überschriften etc.", #AH Website Meta-Details + "Zeitstempel des letzten Website-Scraping/Summarization-Versuchs (für AF, AG, AH).", #AI Website Scrape Timestamp + "Status der URL-Prüfung (z.B. 'URL_CHECK_NEEDED', 'URL_OK', 'FEHLER_SSL'). Wird von 'check_urls' Modus gesetzt/genutzt.", #AJ URL Prüfstatus + "Branchenvorschlag von ChatGPT basierend auf allen verfügbaren Informationen.", #AK Chat Vorschlag Branche + "Konfidenz des ChatGPT-Branchenvorschlags (AK), z.B. Hoch/Mittel/Niedrig.", #AL Chat Branche Konfidenz + "Konsistenzprüfung: 'ok', wenn ChatGPT-Vorschlag (AK) mit CRM-Branche (I) übereinstimmt (unter Berücksichtigung von Parent-Logik).", #AM Chat Konsistenz Branche + "Von ChatGPT generierte Begründung für den Branchenvorschlag (AK) oder bei Abweichung.", #AN Chat Begruendung Abweichung Branche + "Bewertung der FSM-Relevanz durch ChatGPT. ('OK' für relevant, 'X' für irrelevant) (Platzhalter)", #AO Chat Prüfung FSM Relevanz + "Von ChatGPT generierte Begründung für die Einschätzung der FSM-Relevanz. (Platzhalter)", #AP Chat Begründung für FSM Relevanz + "Von ChatGPT geschätzte Anzahl der Mitarbeiter. (Platzhalter)", #AQ Chat Schaetzung Anzahl Mitarbeiter + "Konsistenzprüfung der von ChatGPT geschätzten Mitarbeiterzahl (AQ). (Platzhalter)", #AR Chat Konsistenzprüfung Mitarbeiterzahl + "Von ChatGPT generierte Begründung bei signifikanter Abweichung der geschätzten Mitarbeiterzahl. (Platzhalter)", #AS Chat Begründung Abweichung Mitarbeiterzahl + "Anzahl der Servicetechniker geschätzt durch Chat GPT (z.B. 0, <50, >100). (Platzhalter)", #AT Chat Einschätzung Anzahl Servicetechniker + "Begründung für Abweichungen der ChatGPT-Technikerschätzung zur CRM-Angabe. (Platzhalter)", #AU Chat Begründung Abweichung Anzahl Servicetechniker + "Umsatz durch ChatGPT geschätzt. (Platzhalter)", #AV Chat Schätzung Umsatz + "Begründung für Abweichungen des ChatGPT-Umsatzes zu CRM/Wiki. (Platzhalter)", #AW Chat Begründung Abweichung Umsatz + "Anzahl der Kontakte die zur Suche 'Serviceleiter', 'Leiter Service', 'technischer Leiter', 'Service Manager', 'Leiter Kundendienst' gefunden wurden.", #AX Linked Serviceleiter gefunden + "Anzahl der Kontakte die zur Suche 'Leiter IT', 'IT Leiter', 'Head of IT', 'IT-Leiter', 'CIO' gefunden wurden.", #AY Linked It-Leiter gefunden + "Anzahl der Kontakte die zur Suche 'Geschäftsführer', 'Geschäftsführung', 'GF', 'CEO', 'Geschäftsführerin', 'Managing Director', 'Geschäftsführender Gesellschafter' gefunden wurden.", #AZ Linked Management gefunden + "Anzahl der Kontakte die zur Suche 'Disponent', 'Einsatzplaner' gefunden wurden.", #BA Linked Disponent gefunden + "Timestamp des Zeitpunkts zu dem die Kontaktsuche via SerpAPI für diese Zeile fertiggestellt wurde.", #BB Contact Search Timestamp + "Konsolidierter Umsatzwert in Millionen Euro. Priorisiert Wiki (V) > CRM (L). Berücksichtigt Parent-Account (D).", #BC Finaler Umsatz (Wiki>CRM) + "Konsolidierte Mitarbeiterzahl (absolut). Priorisiert Wiki (W) > CRM (M). Berücksichtigt Parent-Account (D).", #BD Finaler Mitarbeiter (Wiki>CRM) + "Ergebnis der Schätzung durch das trainierte Machine-Learning-Modell (Techniker-Bucket).", #BE Geschaetzter Techniker Bucket + "Plausibilitätsstatus für den finalen Umsatzwert (BC) (z.B. OK, WARNUNG_HOCH, FEHLER_FORMAT).", #BF Plausibilität Umsatz + "Plausibilitätsstatus für die finale Mitarbeiterzahl (BD) (z.B. OK, WARNUNG_NIEDRIG).", #BG Plausibilität Mitarbeiter + "Plausibilitätsstatus für die Umsatz-pro-Mitarbeiter-Ratio (BC/BD).", #BH Plausibilität Umsatz/MA Ratio + "Indikator für Abweichung (>30%) zwischen CRM-Umsatz (L) und Wiki-Umsatz (V). Berücksichtigt Parent-Logik.", #BI Abweichung Umsatz CRM/Wiki + "Indikator für Abweichung (>30%) zwischen CRM-MA (M) und Wiki-MA (W). Berücksichtigt Parent-Logik.", #BJ Abweichung MA CRM/Wiki + "Gesammelte Begründungen für Plausibilitätswarnungen oder -fehler aus den Spalten BF-BJ.", #BK Plausibilität Begründung + "Zeitstempel des letzten Laufs der Plausibilitäts-Checks für diese Zeile.", #BL Plausibilität Prüfdatum + "Timestamp des Zeitpunkts zu dem die übergreifende Validierung/Schätzung durch ChatGPT (AK-AW) durchgeführt wurde.", #BM Timestamp letzte Prüfung + "Systemspalte zur Ausgabe der Skriptversion die das Ergebnis generiert hat.", #BN Version + "Zeigt an, wie viele Tokens für die OpenAI API-Requests dieser Zeile ungefähr benötigt wurden." #BO Tokens ], - [ # Zeile 5: Aufgabe / Funktion - "Datenquelle/Prozesssteuerung: 'x' markiert Zeile für Re-Evaluation in Modus 'reeval'.", #A + [ # Zeile 5: Aufgabe / Funktion (67 Spalten - JETZT AUSFÜHRLICH) + "Datenquelle/Prozesssteuerung: 'x' markiert Zeile für Re-Evaluation im Modus 'reeval'.", #A "Datenquelle: Firmenname aus CRM.", #B - "Datenquelle: Manuell gepflegte Kurzform des Firmennamens, primär für API-Suchen (LinkedIn, SerpAPI) genutzt.", #C - "Datenquelle/Ziel: Website des Unternehmens. Kann durch 'website_lookup' oder 'check_urls' aktualisiert werden.", #D - "Datenquelle: Ort des Unternehmens aus CRM.", #E - "Datenquelle: Land des Unternehmenssitzes laut CRM (manuell pflegen). Wichtig für DACH-Region-Analyse.", #F - "Datenquelle: Beschreibung aus CRM. Input für KI-Analysen.", #G - "Datenquelle: Branchenkategorie aus CRM. Referenz für KI-Branchenbewertung.", #H - "Datenquelle: Externe Branchenbeschreibung (z.B. Dealfront). Input für KI-Branchenbewertung.", #I - "Datenquelle: Recherchierte Anzahl Servicetechniker. Dient als Ground Truth für ML-Training und Validierung der KI-Schätzung.", #J - "Datenquelle: Umsatz aus CRM. Input für Konsolidierung und ML.", #K - "Datenquelle: Mitarbeiterzahl aus CRM. Input für Konsolidierung und ML.", #L - "Datenquelle: Alte/vorgeschlagene Wiki-URL aus CRM. Dient als initialer Input für Wiki-Prozess.", #M - "Ziel/Quelle: Finale, als relevant erachtete Wikipedia-URL. Quelle: Wikipedia-Scraper (Suche/Extraktion), SerpAPI (Neusuche), ChatGPT (Vorschlag AA nach Prüfung), oder manuelle Eingabe.", #N - "Quelle: Aus Wikipedia-Infobox extrahierte Stadt des Unternehmenssitzes. Ziel: Geografische Analyse.", #O - "Quelle: Aus Wikipedia-Infobox extrahiertes Land des Unternehmenssitzes. Ziel: Geografische Analyse (DACH).", #P - "Quelle: Erster aussagekräftiger Absatz des Wikipedia-Artikels (N). Input für KI-Analysen, z.B. Wiki-Verifizierung.", #Q - "Quelle: Aus der Wikipedia-Infobox extrahierte Branche(n). Input für KI-Branchenbewertung.", #R - "Quelle: Aus der Wikipedia-Infobox extrahierter Umsatz. Input für Konsolidierung und ML.", #S - "Quelle: Aus der Wikipedia-Infobox extrahierte Mitarbeiterzahl. Input für Konsolidierung und ML.", #T - "Quelle: Wikipedia-Kategorien des Artikels (N). Input für KI-Analysen, z.B. Wiki-Verifizierung und Branchenbewertung.", #U - "System: Timestamp der letzten Wikipedia-Suche/Datenextraktion (für N-U). Steuert Wiederholung von Wiki-Extraktion.", #V - "System: Timestamp der letzten ChatGPT-Verifizierung des Wiki-Artikels (N) (Ergebnis in Y-AA). Steuert Wiederholung der Verifizierung.", #W - "System: Timestamp des letzten Versuchs, eine fehlende Wiki-URL (N) via SerpAPI zu suchen. Steuert Wiederholung der SerpAPI-Suche.", #X - "Ziel: Ergebnis der ChatGPT-Konsistenzprüfung für Wiki-Artikel (N). Status '?' triggert erneute Prüfung im Modus 'wiki_verify'.", #Y - "Ziel: Begründung von ChatGPT, falls Wiki-Artikel (N) als inkonsistent bewertet wurde.", #Z - "Ziel: Von ChatGPT vorgeschlagene alternative Wikipedia-URL. Wird von Modus 'update_wiki_suggestions' verarbeitet.", #AA - "Ziel/Info: Wird aktuell primär geleert. Kann für spezifische Begründungen bei URL-Abweichungen genutzt werden.", #AB - "Ziel: Rohtext der Website. Input für Zusammenfassung (AD).", #AC - "Ziel: KI-generierte Zusammenfassung des Website-Rohtextes (AC). Input für Branchenbewertung (AH).", #AD - "Ziel: Strukturierte Meta-Daten der Website (Title, Description, H-Tags). Für schnelle Analyse & Validierung.", #AE - "System: Timestamp des letzten Website-Scraping/Summarization-Versuchs (für AC, AD, AE). Steuert Wiederholung.", #AF - "System/Ziel: Status der URL-Prüfung. 'URL_CHECK_NEEDED' triggert Neusuche im Modus 'check_urls'. Wird dort aktualisiert.", #AG - "Ziel: Von ChatGPT final vorgeschlagene Branche gemäß Zielschema. Hauptziel der Branchenklassifizierung.", #AH - "Ziel: Von ChatGPT eingeschätzte Konfidenz (Hoch/Mittel/Niedrig) für den Branchenvorschlag (AH).", #AI (NEU) - "Ziel: Ergebnis des Abgleichs zwischen KI-Branchenvorschlag (AH) und CRM-Branche (H).", #AJ - "Ziel: Von ChatGPT generierte Begründung für den Branchenvorschlag (AH).", #AK - "Ziel: Ergebnis der FSM-Relevanzprüfung durch ChatGPT. (Platzhalter)", #AL - "Ziel: Begründung von ChatGPT für die FSM-Relevanz. (Platzhalter)", #AM - "Ziel: Von ChatGPT geschätzte Mitarbeiterzahl. (Platzhalter)", #AN - "Ziel: Konsistenzprüfung der KI-Mitarbeiterschätzung. (Platzhalter)", #AO - "Ziel: Begründung von ChatGPT bei Abweichung der Mitarbeiterschätzung. (Platzhalter)", #AP - "Ziel: Von ChatGPT geschätzte Anzahl Servicetechniker. (Platzhalter)", #AQ - "Ziel: Begründung von ChatGPT bei Abweichung der Technikerzahl-Schätzung. (Platzhalter)", #AR - "Ziel: Von ChatGPT geschätzter Umsatz. (Platzhalter)", #AS - "Ziel: Begründung von ChatGPT bei Abweichung der Umsatzschätzung. (Platzhalter)", #AT - "Ziel: Anzahl gefundener LinkedIn-Kontakte (Serviceleiter) via SerpAPI.", #AU - "Ziel: Anzahl gefundener LinkedIn-Kontakte (IT-Leiter) via SerpAPI.", #AV - "Ziel: Anzahl gefundener LinkedIn-Kontakte (Management) via SerpAPI.", #AW - "Ziel: Anzahl gefundener LinkedIn-Kontakte (Disponent) via SerpAPI.", #AX - "System: Timestamp der letzten Kontaktsuche (Modus 'contacts'). Steuert Wiederholung.", #AY - "Ziel: Konsolidierter Umsatz (Wiki-Wert (S) > CRM-Wert (K)). Input für ML-Modell.", #AZ - "Ziel: Konsolidierte Mitarbeiterzahl (Wiki-Wert (T) > CRM-Wert (L)). Input für ML-Modell.", #BA - "Vom ML-Modell vorhergesagter Bucket für die Anzahl der Servicetechniker.", #BB - "Ziel: Kennzeichnung der Plausibilität des finalen Umsatzwertes.", #BC - "Ziel: Kennzeichnung der Plausibilität der finalen Mitarbeiterzahl.", #BD - "Ziel: Kennzeichnung der Plausibilität der Umsatz-pro-Mitarbeiter-Verhältniszahl.", #BE - "Ziel: Indikator zur Datenqualität bezüglich Umsatz CRM vs. Wiki.", #BF - "Ziel: Indikator zur Datenqualität bezüglich Mitarbeiter CRM vs. Wiki.", #BG - "Ziel: Zusammenfassung der Gründe für erkannte Plausibilitätsprobleme.", #BH - "System: Timestamp des letzten Plausibilitäts-Checks. Steuert Wiederholung.", #BI - "System: Timestamp der letzten übergreifenden ChatGPT-Evaluation (Branchen, FSM etc.). Steuert Wiederholung.", #BJ - "System: Skriptversion, die die Zeile zuletzt signifikant bearbeitet hat.", #BK - "System: Verbrauchte Tokens für OpenAI API-Aufrufe für diese Zeile." #BL + "Datenquelle: Manuell gepflegte Kurzform des Firmennamens, primär für API-Suchen (LinkedIn, SerpAPI) und Matching genutzt.", #C + "Datenquelle: Manuell oder aus CRM gepflegter Name der Muttergesellschaft. Wird verwendet, um bei Konsolidierung und Plausibilitätsabgleich von Tochterfirmen korrekte Bezüge herzustellen.", #D + "Datenquelle/Ziel: Offizielle Website des Unternehmens. Wird für Web-Scraping und als Info für ChatGPT genutzt. Kann durch 'website_lookup' oder 'check_urls' aktualisiert werden.", #E + "Datenquelle: Ort des Unternehmenssitzes aus CRM.", #F + "Datenquelle: Land des Unternehmenssitzes laut CRM. Wichtig für regionale Analysen.", #G + "Datenquelle: Beschreibung aus CRM. Wichtiger Input für KI-Analysen (Branchen, FSM etc.).", #H + "Datenquelle: Branchenkategorie aus CRM. Dient als Referenz und für Vergleiche mit KI-Vorschlägen.", #I + "Datenquelle: Externe Branchenbeschreibung (z.B. von Dealfront). Zusätzlicher Input für KI-Branchenbewertung.", #J + "Datenquelle: Recherchierte Anzahl Servicetechniker. Dient als Ground Truth für ML-Training und Validierung der KI-Schätzung.", #K + "Datenquelle: Umsatz aus CRM (in Mio. €). Input für Konsolidierung, Plausi-Checks und ML.", #L + "Datenquelle: Mitarbeiterzahl aus CRM (absolut). Input für Konsolidierung, Plausi-Checks und ML.", #M + "Datenquelle: Alte/vorgeschlagene Wiki-URL aus CRM. Dient als initialer Input oder Vergleichswert für den Wiki-Prozess.", #N + "Ziel/System: Vom Skript generierter Vorschlag für die Muttergesellschaft (basierend auf Heuristiken), zur manuellen Prüfung.", #O + "Prozesssteuerung/Manuell: Bestätigung ('x'), Ablehnung ('-') oder Unklarheit ('?') des System-Vorschlags für den Parent Account. Kann Übernahme in Spalte D steuern.", #P + "Ziel/Quelle: Finale, als relevant erachtete Wikipedia-URL. Quelle: Wikipedia-Scraper, SerpAPI, ChatGPT-Vorschlag (AD), oder manuelle Eingabe.", #Q + "Quelle: Aus Wikipedia-Infobox extrahierte Stadt des Unternehmenssitzes. Ziel: Geografische Analyse.", #R + "Quelle: Aus Wikipedia-Infobox extrahiertes Land des Unternehmenssitzes. Ziel: Geografische Analyse (DACH).", #S + "Quelle: Erster aussagekräftiger Absatz des Wikipedia-Artikels (Q). Input für KI-Analysen (Wiki-Verifizierung, Branchen).", #T + "Quelle: Aus der Wikipedia-Infobox extrahierte Branche(n). Input für KI-Branchenbewertung.", #U + "Quelle: Aus der Wikipedia-Infobox extrahierter Umsatz (in Mio. €). Input für Konsolidierung und Plausi-Checks.", #V + "Quelle: Aus der Wikipedia-Infobox extrahierte Mitarbeiterzahl (absolut). Input für Konsolidierung und Plausi-Checks.", #W + "Quelle: Wikipedia-Kategorien des Artikels (Q). Wichtiger Input für KI-Analysen (Wiki-Verifizierung, Branchenbewertung, Parent-Vorschlag).", #X + "System: Timestamp der letzten Wikipedia-Suche/Datenextraktion (für Spalten Q-X). Steuert Wiederholung.", #Y + "System: Timestamp der letzten ChatGPT-Verifizierung des Wiki-Artikels (Q). Steuert Wiederholung der Verifizierung.", #Z + "System: Timestamp des letzten Versuchs, eine fehlende Wiki-URL (Q) via SerpAPI zu suchen. Steuert Wiederholung.", #AA + "Ziel: Ergebnis der ChatGPT-Konsistenzprüfung für Wiki-Artikel (Q). Status '?' triggert erneute Prüfung.", #AB + "Ziel: Begründung von ChatGPT, falls Wiki-Artikel (Q) als inkonsistent bewertet wurde.", #AC + "Ziel: Von ChatGPT vorgeschlagene alternative Wikipedia-URL. Wird von Modus 'update_wiki_suggestions' verarbeitet.", #AD + "Ziel/Info: Begründung für manuelle oder systemische Abweichungen bei der finalen Wiki-URL-Wahl.", #AE + "Ziel: Rohtext der Website (AF). Input für Zusammenfassung (AG).", #AF + "Ziel: KI-generierte Zusammenfassung des Website-Rohtextes (AF). Input für Branchenbewertung.", #AG + "Ziel: Strukturierte Meta-Daten der Website (Title, Description, H-Tags). Für schnelle Analyse & Validierung.", #AH + "System: Timestamp des letzten Website-Scraping/Summarization-Versuchs (für AF-AH). Steuert Wiederholung.", #AI + "System/Ziel: Status der URL-Prüfung. 'URL_CHECK_NEEDED' triggert Neusuche im Modus 'check_urls'.", #AJ + "Ziel: Von ChatGPT final vorgeschlagene Branche gemäß Zielschema. Hauptziel der Branchenklassifizierung.", #AK + "Ziel: Von ChatGPT eingeschätzte Konfidenz (Hoch/Mittel/Niedrig) für den Branchenvorschlag (AK).", #AL + "Ziel: Ergebnis des Abgleichs ('ok'/'X') zwischen KI-Branchenvorschlag (AK) und CRM-Branche (I).", #AM + "Ziel: Von ChatGPT generierte Begründung für den Branchenvorschlag (AK) und/oder die Konsistenz (AM).", #AN + "Ziel: Ergebnis der FSM-Relevanzprüfung durch ChatGPT. (Platzhalter)", #AO + "Ziel: Begründung von ChatGPT für die FSM-Relevanz. (Platzhalter)", #AP + "Ziel: Von ChatGPT geschätzte Mitarbeiterzahl. (Platzhalter)", #AQ + "Ziel: Konsistenzprüfung der KI-Mitarbeiterschätzung. (Platzhalter)", #AR + "Ziel: Begründung von ChatGPT bei Abweichung der Mitarbeiterschätzung. (Platzhalter)", #AS + "Ziel: Von ChatGPT geschätzte Anzahl Servicetechniker (Buckets). (Platzhalter)", #AT + "Ziel: Begründung von ChatGPT bei Abweichung der Technikerzahl-Schätzung. (Platzhalter)", #AU + "Ziel: Von ChatGPT geschätzter Umsatz. (Platzhalter)", #AV + "Ziel: Begründung von ChatGPT bei Abweichung der Umsatzschätzung. (Platzhalter)", #AW + "Ziel: Anzahl gefundener LinkedIn-Kontakte (Serviceleiter) via SerpAPI.", #AX + "Ziel: Anzahl gefundener LinkedIn-Kontakte (IT-Leiter) via SerpAPI.", #AY + "Ziel: Anzahl gefundener LinkedIn-Kontakte (Management) via SerpAPI.", #AZ + "Ziel: Anzahl gefundener LinkedIn-Kontakte (Disponent) via SerpAPI.", #BA + "System: Timestamp der letzten Kontaktsuche (Modus 'contacts'). Steuert Wiederholung.", #BB + "Ziel: Konsolidierter Umsatz (Wiki > CRM), berücksichtigt Parent-Struktur. Input für ML und Plausi.", #BC + "Ziel: Konsolidierte Mitarbeiterzahl (Wiki > CRM), berücksichtigt Parent-Struktur. Input für ML und Plausi.", #BD + "Ziel: Vom ML-Modell vorhergesagter Bucket für die Anzahl der Servicetechniker.", #BE + "Ziel: Kennzeichnung der Plausibilität des finalen Umsatzwertes (BC).", #BF + "Ziel: Kennzeichnung der Plausibilität der finalen Mitarbeiterzahl (BD).", #BG + "Ziel: Kennzeichnung der Plausibilität der Umsatz-pro-Mitarbeiter-Verhältniszahl.", #BH + "Ziel: Indikator zur Datenqualität bezüglich Umsatz CRM (L) vs. Wiki (V), berücksichtigt Parent.", #BI + "Ziel: Indikator zur Datenqualität bezüglich Mitarbeiter CRM (M) vs. Wiki (W), berücksichtigt Parent.", #BJ + "Ziel: Zusammenfassung der Gründe für erkannte Plausibilitätsprobleme (aus BF-BJ).", #BK + "System: Timestamp des letzten Plausibilitäts-Checks. Steuert Wiederholung des Plausi-Modus.", #BL + "System: Timestamp der letzten übergreifenden ChatGPT-Evaluation (AK-AW). Steuert Wiederholung.", #BM + "System: Skriptversion, die die Zeile zuletzt signifikant bearbeitet hat.", #BN + "System: Verbrauchte Tokens für OpenAI API-Aufrufe für diese Zeile (kumuliert)." #BO ] ] - + num_cols = len(new_headers[0]) if not all(len(row) == num_cols for row in new_headers): logger.critical(f"FEHLER in alignment_demo: Die Anzahl der Spalten in den Header-Zeilen ist nicht konsistent! Erwartet {num_cols} Spalten pro Zeile, aber Längen sind: {[len(row) for row in new_headers]}.") - num_cols = max(len(row) for row in new_headers) if new_headers else 0 + num_cols = max(len(row) for row in new_headers) if new_headers and any(row for row in new_headers) else 0 # Sicherstellen, dass new_headers nicht komplett leer ist if num_cols == 0: logger.error("FEHLER: Konnte keine Spaltenanzahl für Alignment-Demo Header bestimmen.") return @@ -2713,7 +2716,7 @@ def alignment_demo(sheet): string = chr(65 + remainder) + string return string - end_col_letter = colnum_string(num_cols) + end_col_letter = colnum_string(num_cols) # Sollte "BO" für 67 Spalten sein header_range = f"A1:{end_col_letter}{len(new_headers)}" logger.info(f"Schreibe Alignment-Demo Header in Bereich {header_range}...") @@ -7915,253 +7918,218 @@ class DataProcessor: +# Innerhalb der DataProcessor Klasse def _get_numeric_value_for_plausi(self, value_str, is_umsatz=False): + # Diese Funktion ist jetzt sehr ähnlich zu get_numeric_filter_value, + # nur dass sie np.nan für "unbekannt" zurückgibt und den Umsatz immer in Euro. + if value_str is None or pd.isna(value_str): return np.nan raw_value_str_clean = str(value_str).strip() # Explizit "0" und andere "unbekannt" Strings als NaN - if raw_value_str_clean.lower() in ['', 'k.a.', 'n/a', '-', '0', '0.0', '0,00', '0.000']: + if raw_value_str_clean.lower() in ['', 'k.a.', 'n/a', '-', '0', '0.0', '0,00', '0,000', '0.00']: # '.00' hinzugefügt self.logger.debug(f"PlausiParse: Input '{raw_value_str_clean}' -> NaN (als 'unbekannt' interpretiert).") return np.nan + # Bereinigungslogik temp_val = clean_text(raw_value_str_clean) if temp_val.lower() in ['k.a.', 'n/a', '-']: return np.nan - temp_val = re.sub(r'(?i)^\s*(ca\.?|circa|etwa|rund|ueber|unter|mehr als|weniger als|bis zu)\s+', '', temp_val) - temp_val = re.sub(r'[€$£¥]', '', temp_val).strip() # Währungssymbole entfernen - temp_val = re.split(r'\s*(-|–|bis)\s*', temp_val, 1)[0].strip() # Nur ersten Teil bei Spannen + temp_val = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|ueber|unter|mehr als|weniger als|bis zu)\s+', '', temp_val) + temp_val = re.sub(r'[€$£¥]', '', temp_val).strip() + temp_val = re.split(r'\s*(-|–|bis)\s*', temp_val, 1)[0].strip() - # Robuste Behandlung von Tausendertrennern (Punkte) und Dezimalkomma - # Entferne zuerst alle Punkte, die als Tausendertrenner dienen könnten - # Ein Punkt ist Tausendertrenner, wenn rechts davon 3 Ziffern und dann ein weiteres Nicht-Ziffer-Zeichen (oder Ende) oder ein weiteres Komma steht - # Oder wenn mehrere Punkte vorhanden sind und kein Komma. + # Tausendertrenner und Dezimalzeichen normalisieren + num_extraction_str = temp_val.replace("'", "") + if '.' in num_extraction_str and ',' in num_extraction_str: + if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): + num_extraction_str = num_extraction_str.replace(',', '') + else: + num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') + elif ',' in num_extraction_str: + num_extraction_str = num_extraction_str.replace(',', '.') + elif '.' in num_extraction_str: + if re.fullmatch(r'^\d{1,3}(\.\d{3})+$', num_extraction_str): + num_extraction_str = num_extraction_str.replace('.', '') + elif num_extraction_str.count('.') > 1: + self.logger.debug(f"PlausiParse: Mehrere Punkte in '{num_extraction_str}' (nicht als Tausenderformat erkannt).") + return np.nan - # Vereinfachte Tausendertrenner-Logik, die für Fälle wie "4.380" und "17.800" funktionieren sollte: - # Annahme: Wenn ein Punkt vorkommt und kein Komma, oder das letzte Komma vor dem letzten Punkt steht, - # dann sind Punkte Tausendertrenner. - - cleaned_num_str = temp_val - if '.' in cleaned_num_str and ',' in cleaned_num_str: - if cleaned_num_str.rfind('.') > cleaned_num_str.rfind(','): # US-Stil: 1,234.56 -> Kommas entfernen - cleaned_num_str = cleaned_num_str.replace(',', '') - else: # EU-Stil: 1.234,56 -> Punkte entfernen, Komma zu Punkt - cleaned_num_str = cleaned_num_str.replace('.', '').replace(',', '.') - elif ',' in cleaned_num_str: # Nur Komma: 1234,56 -> 1234.56 - cleaned_num_str = cleaned_num_str.replace(',', '.') - elif '.' in cleaned_num_str: # Nur Punkte: - # Wenn es aussieht wie x.xxx (z.B. 4.380, 17.800), entferne den Punkt - if re.match(r'^\d{1,3}(\.\d{3})+$', cleaned_num_str): # Matches 1.234 or 1.234.567 etc. - cleaned_num_str = cleaned_num_str.replace('.', '') - # Ansonsten ist es ein Dezimalpunkt (z.B. 123.45) - bleibt bestehen - - match = re.search(r'([\d.\-]+)', cleaned_num_str) # Erlaube Dezimalpunkt und Minus + match = re.search(r'([\d.\-]+)', num_extraction_str) if not match: - self.logger.debug(f"PlausiParse: Kein numerischer Match in '{cleaned_num_str}' (von '{raw_value_str_clean}') -> NaN.") + self.logger.debug(f"PlausiParse: Kein numerischer Match in '{num_extraction_str}' (von '{raw_value_str_clean}') -> NaN.") return np.nan - num_str_from_regex = match.group(1) + num_str_for_float = match.group(1) try: - # Vermeide Interpretation von "...." als Zahl - if not num_str_from_regex or num_str_from_regex == '.' or (num_str_from_regex.count('.') > 1 and not num_str_from_regex.endswith('.')): + if not num_str_for_float or num_str_for_float == '.' or num_str_for_float == '-' or \ + (num_str_for_float.count('.') > 1) or \ + (num_str_for_float.count('-') > 1) or \ + (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): raise ValueError("Ungültiger Zahlenstring nach Regex") - num = float(num_str_from_regex) # Das ist die Zahl, wie sie im String steht (z.B. 173, 4380, 17800) + num_val = float(num_str_for_float) # Der reine Zahlenwert aus dem String + # Einheiten-Skalierung zum absoluten Wert original_lower = raw_value_str_clean.lower() - final_num_absolute = num + final_num_absolute = num_val if is_umsatz: - # Input ist bereits in Mio. €, es sei denn, es gibt explizite andere Einheiten + # Annahme: Input-String (value_str) ist bereits in Mio, wenn keine andere Einheit explizit genannt ist. + # Wir wollen hier den absoluten Euro-Betrag. if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - final_num_absolute = num * 1000000000.0 # z.B. "2 Mrd" -> 2 * 10^9 + final_num_absolute = num_val * 1000000000.0 # z.B. String "2 Mrd" -> num_val=2 -> 2 Mrd € elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - final_num_absolute = num * 1000.0 # z.B. "500 Tsd" -> 500 * 10^3 - else: # Annahme: num ist bereits in Mio, konvertiere zu absolutem Euro-Wert - final_num_absolute = num * 1000000.0 - else: # Mitarbeiter (absolute Zahl, es sei denn, explizite Einheiten) - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): final_num_absolute = num * 1000000000.0 - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): final_num_absolute = num * 1000000.0 - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): final_num_absolute = num * 1000.0 + final_num_absolute = num_val * 1000.0 # z.B. String "500 Tsd" -> num_val=500 -> 500 Tsd € + else: # Keine explizite andere Einheit, oder "Mio" steht da + final_num_absolute = num_val * 1000000.0 # z.B. String "173" -> num_val=173 -> 173 Mio € + else: # Mitarbeiter (absolute Zahl erwartet, außer bei expliziten Einheiten) + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): final_num_absolute = num_val * 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): final_num_absolute = num_val * 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): final_num_absolute = num_val * 1000.0 - # "0" (als String) wurde oben schon zu NaN. - # Wenn final_num_absolute jetzt numerisch 0 ist, war es z.B. "0 Tsd". + # Nach der Skalierung: Wenn das Ergebnis 0 ist, aber der Originalstring nicht explizit "0" war, ist 0 ein valider Wert. + # Explizite "0"-Strings wurden ganz am Anfang schon zu NaN. + # Diese Funktion soll den *berechneten* numerischen Wert zurückgeben. return final_num_absolute except ValueError as e: - self.logger.debug(f"PlausiParse: ValueError '{e}' bei Konvertierung von '{num_str_from_regex}' (von '{raw_value_str_clean}') -> NaN.") + self.logger.debug(f"PlausiParse: ValueError '{e}' bei Konvertierung von '{num_str_for_float}' (von '{raw_value_str_clean}') -> NaN.") + return np.nan + except Exception as e_general: + self.logger.error(f"Unerwarteter Fehler in _get_numeric_value_for_plausi für '{raw_value_str_clean[:50]}...': {e_general}") + self.logger.debug(traceback.format_exc()) return np.nan def _check_financial_plausibility(self, row_data_dict): - """ - Prüft die Plausibilität der finalen Umsatz- und Mitarbeiterzahlen - sowie deren Ratio und den Abgleich mit CRM-Daten. - """ - results = { - "plaus_umsatz_flag": "NICHT_PRUEFBAR", - "plaus_ma_flag": "NICHT_PRUEFBAR", - "plaus_ratio_flag": "NICHT_PRUEFBAR", - "abweichung_umsatz_flag": "N/A", - "abweichung_ma_flag": "N/A", - "begruendungen": [] + results = { # Defaults + "plaus_umsatz_flag": "NICHT_PRUEFBAR", "plaus_ma_flag": "NICHT_PRUEFBAR", + "plaus_ratio_flag": "NICHT_PRUEFBAR", "abweichung_umsatz_flag": "N/A", + "abweichung_ma_flag": "N/A", "begruendungen": [] } final_umsatz_str = row_data_dict.get("Finaler Umsatz (Wiki>CRM)", "k.A.") final_ma_str = row_data_dict.get("Finaler Mitarbeiter (Wiki>CRM)", "k.A.") + self.logger.debug(f" PlausiCheck Input: final_umsatz_str='{final_umsatz_str}', final_ma_str='{final_ma_str}'") - self.logger.debug(f" _check_financial_plausibility Input: final_umsatz_str='{final_umsatz_str}', final_ma_str='{final_ma_str}'") + # Werte für absolute Plausi-Checks (Umsatz in Euro, MA absolut) + umsatz_num_absolut = self._get_numeric_value_for_plausi(final_umsatz_str, is_umsatz=True) + ma_num_absolut = self._get_numeric_value_for_plausi(final_ma_str, is_umsatz=False) + self.logger.debug(f" PlausiCheck Numerisch (Absolut): umsatz_abs={umsatz_num_absolut}, ma_abs={ma_num_absolut}") - umsatz_num = self._get_numeric_value_for_plausi(final_umsatz_str, is_umsatz=True) - ma_num = self._get_numeric_value_for_plausi(final_ma_str, is_umsatz=False) + # --- Plausi-Checks für absolute Werte und Ratio (verwenden umsatz_num_absolut, ma_num_absolut) --- + # (Dieser Teil bleibt wie im letzten Vorschlag, der die Debug-Blöcke enthielt, + # aber er arbeitet jetzt mit umsatz_num_absolut und ma_num_absolut) - self.logger.debug(f" _check_financial_plausibility Numerisch: umsatz_num={umsatz_num}, ma_num={ma_num}") - - # 1. Plausibilität Umsatz - if pd.isna(umsatz_num): - self.logger.debug(f" Format-Check Umsatz: final_umsatz_str='{final_umsatz_str}' (Typ: {type(final_umsatz_str)})") - self.logger.debug(f" Format-Check Umsatz: umsatz_num ist NaN? {pd.isna(umsatz_num)}") - exclusion_list_umsatz = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000'] - - # Explizite Prüfung des Stripped-Lowered-Strings - cleaned_final_umsatz_str = final_umsatz_str.lower().strip() - is_excluded_umsatz = cleaned_final_umsatz_str in exclusion_list_umsatz - - self.logger.debug(f" Format-Check Umsatz: Cleaned Input String für Exklusion: '{cleaned_final_umsatz_str}'") - self.logger.debug(f" Format-Check Umsatz: '{cleaned_final_umsatz_str}' in exclusion_list_umsatz? {is_excluded_umsatz}") - - if not is_excluded_umsatz: + # 1. Plausibilität Umsatz (absolut) + if pd.isna(umsatz_num_absolut): + exclusion_list = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] + if final_umsatz_str.lower().strip() not in exclusion_list: results["plaus_umsatz_flag"] = "FEHLER_FORMAT" - results["begruendungen"].append(f"Finaler Umsatz ('{final_umsatz_str}') -> num: NaN. String '{cleaned_final_umsatz_str}' nicht in Exklusionsliste.") - self.logger.warning(f" !!! FEHLER_FORMAT für Umsatz gesetzt bei final_umsatz_str='{final_umsatz_str}' (cleaned='{cleaned_final_umsatz_str}') !!!") # WARNUNG loggen - else: - self.logger.debug(f" Format-Check Umsatz: Input '{cleaned_final_umsatz_str}' ist in Exklusionsliste. Kein Formatfehler, Flag bleibt '{results['plaus_umsatz_flag']}'.") - else: - results["plaus_umsatz_flag"] = "OK" - if umsatz_num == 0: + results["begruendungen"].append(f"Finaler Umsatz ('{final_umsatz_str}') konnte nicht als Zahl interpretiert werden (und war kein 'k.A.' oder '0').") + else: # umsatz_num_absolut ist eine gültige Zahl + results["plaus_umsatz_flag"] = "OK" + if umsatz_num_absolut == 0: # Hier ist 0 ein echter numerischer Wert results["plaus_umsatz_flag"] = "WARNUNG_NULL_WERT" - results["begruendungen"].append(f"Finaler Umsatz ist numerisch 0 (aus ursprünglichem Sheet-Wert: '{final_umsatz_str}').") - elif umsatz_num < getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000): + results["begruendungen"].append(f"Finaler Umsatz ist numerisch 0 (aus '{final_umsatz_str}').") + elif umsatz_num_absolut < getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000): results["plaus_umsatz_flag"] = "WARNUNG_NIEDRIG" - results["begruendungen"].append(f"Finaler Umsatz ({umsatz_num:,.0f} €) < {getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000):,.0f} €.") - elif umsatz_num > getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000): + # ... Begründung ... + elif umsatz_num_absolut > getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000): results["plaus_umsatz_flag"] = "WARNUNG_HOCH" - results["begruendungen"].append(f"Finaler Umsatz ({umsatz_num:,.0f} €) > {getattr(Config, 'PLAUSI_UMSATZ_MAX_WARNUNG', 200000000000):,.0f} €.") - - # 2. Plausibilität Mitarbeiter - if pd.isna(ma_num): - # --- BEGINN DEBUG-BLOCK FÜR MA-FORMAT-CHECK --- - self.logger.debug(f" Format-Check MA: final_ma_str='{final_ma_str}' (Typ: {type(final_ma_str)})") - self.logger.debug(f" Format-Check MA: ma_num ist NaN? {pd.isna(ma_num)}") - exclusion_list_ma = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000'] - is_excluded_ma = final_ma_str.lower().strip() in exclusion_list_ma - self.logger.debug(f" Format-Check MA: '{final_ma_str.lower().strip()}' in exclusion_list_ma? {is_excluded_ma}") - # --- ENDE DEBUG-BLOCK FÜR MA FORMAT-CHECK --- - if not is_excluded_ma: - results["plaus_ma_flag"] = "FEHLER_FORMAT" - results["begruendungen"].append(f"Finale MA-Zahl ('{final_ma_str}') konnte nicht als gültige Zahl interpretiert werden (und war kein 'k.A.' oder expliziter '0'-Wert).") - # else: Flag bleibt "NICHT_PRUEFBAR" - else: - results["plaus_ma_flag"] = "OK" - if ma_num == 0: - results["plaus_ma_flag"] = "WARNUNG_NULL_WERT" - results["begruendungen"].append(f"Finale MA-Zahl ist numerisch 0 (aus ursprünglichem Sheet-Wert: '{final_ma_str}').") - elif ma_num < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1): - results["plaus_ma_flag"] = "WARNUNG_NIEDRIG" - results["begruendungen"].append(f"Finale MA-Zahl ({ma_num:.0f}) < {getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1)}.") - elif ma_num > getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000): - results["plaus_ma_flag"] = "WARNUNG_HOCH" - results["begruendungen"].append(f"Finale MA-Zahl ({ma_num:.0f}) > {getattr(Config, 'PLAUSI_MA_MAX_WARNUNG', 1000000)}.") - - if not pd.isna(umsatz_num) and umsatz_num > getattr(Config, 'PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK', 1000000) and \ - ma_num !=0 and ma_num < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3) : - if results["plaus_ma_flag"] == "OK": results["plaus_ma_flag"] = "WARNUNG_RATIO_IMPL" - results["begruendungen"].append(f"Wenige MA ({ma_num:.0f}) bei signifikantem Umsatz ({umsatz_num:,.0f} €).") - - # 3. Umsatz/MA Ratio - if not pd.isna(umsatz_num) and not pd.isna(ma_num): - if ma_num > 0: - ratio = umsatz_num / ma_num - results["plaus_ratio_flag"] = "OK" - if ratio < getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000): - results["plaus_ratio_flag"] = "WARNUNG_RATIO_NIEDRIG" - results["begruendungen"].append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) < {getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MIN', 25000):,.0f}.") - elif ratio > getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000): - results["plaus_ratio_flag"] = "WARNUNG_RATIO_HOCH" - results["begruendungen"].append(f"Umsatz/MA Ratio ({ratio:,.0f} €/MA) > {getattr(Config, 'PLAUSI_RATIO_UMSATZ_PRO_MA_MAX', 1500000):,.0f}.") - elif umsatz_num > 0 and ma_num == 0: # Umsatz da, aber MA ist numerisch 0 - results["plaus_ratio_flag"] = "FEHLER_RATIO_MA_NULL" - results["begruendungen"].append("Ratio nicht berechenbar: Umsatz > 0, aber MA=0.") - if results["plaus_ma_flag"] == "OK" or results["plaus_ma_flag"] == "WARNUNG_NULL_WERT": - results["plaus_ma_flag"] = "WARNUNG_NULL_BEI_UMSATZ" + # ... Begründung ... - # 4. Abgleich CRM vs. Wiki + # 2. Plausibilität Mitarbeiter (absolut) - analog zu Umsatz + if pd.isna(ma_num_absolut): + exclusion_list = ['k.a.', '', 'n/a', '-', '0', '0.0', '0,00', '0.000', '0.00'] + if final_ma_str.lower().strip() not in exclusion_list: + results["plaus_ma_flag"] = "FEHLER_FORMAT" + # ... Begründung ... + else: + results["plaus_ma_flag"] = "OK" + if ma_num_absolut == 0: + results["plaus_ma_flag"] = "WARNUNG_NULL_WERT" + # ... Begründung ... + elif ma_num_absolut < getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_ABS', 1): + results["plaus_ma_flag"] = "WARNUNG_NIEDRIG" + # ... Begründung ... + # ... (Rest der MA-Checks, inkl. spezifischer Check für wenig MA bei Umsatz) + + # 3. Umsatz/MA Ratio (verwendet umsatz_num_absolut, ma_num_absolut) + if not pd.isna(umsatz_num_absolut) and not pd.isna(ma_num_absolut): + if ma_num_absolut > 0: + ratio = umsatz_num_absolut / ma_num_absolut + results["plaus_ratio_flag"] = "OK" + # ... (Ratio-Warnungen basierend auf Config-Werten für PLAUSI_RATIO_UMSATZ_PRO_MA_MIN/MAX) + # ... (Behandlung ma_num_absolut == 0) + + + # --- 4. Abgleich CRM vs. Wiki --- + # HIER VERWENDEN WIR get_numeric_filter_value, um die Mio-Werte für den Vergleich zu bekommen crm_umsatz_str = row_data_dict.get("CRM Umsatz", "k.A.") wiki_umsatz_str = row_data_dict.get("Wiki Umsatz", "k.A.") crm_ma_str = row_data_dict.get("CRM Anzahl Mitarbeiter", "k.A.") wiki_ma_str = row_data_dict.get("Wiki Mitarbeiter", "k.A.") - crm_u = self._get_numeric_value_for_plausi(crm_umsatz_str, True) - wiki_u = self._get_numeric_value_for_plausi(wiki_umsatz_str, True) - crm_m = self._get_numeric_value_for_plausi(crm_ma_str, False) - wiki_m = self._get_numeric_value_for_plausi(wiki_ma_str, False) + # Diese geben jetzt Umsatz in Mio zurück, oder 0 bei Unbekannt/Fehler für den Filter + crm_u_mio = get_numeric_filter_value(crm_umsatz_str, True) + wiki_u_mio = get_numeric_filter_value(wiki_umsatz_str, True) + # Diese geben absolute MA-Zahl zurück, oder 0 bei Unbekannt/Fehler für den Filter + crm_m_abs = get_numeric_filter_value(crm_ma_str, False) + wiki_m_abs = get_numeric_filter_value(wiki_ma_str, False) abweichung_prozent_config = getattr(Config, 'PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT', 30) / 100.0 - # Umsatz Abweichung - if not pd.isna(crm_u) and not pd.isna(wiki_u): - if crm_u == 0 and wiki_u == 0: - results["abweichung_umsatz_flag"] = "OK_BEIDE_NULL" - elif crm_u >= 0 and wiki_u >= 0 : # Erlaube 0 für Vergleich - # Wenn einer der Werte 0 ist und der andere signifikant, ist es eine große Abweichung - # Definiere "signifikant" hier z.B. als > halber MIN_WARNUNG Schwellenwert - schwelle_fuer_null_vergleich = getattr(Config, 'PLAUSI_UMSATZ_MIN_WARNUNG', 50000) / 2 - if (crm_u == 0 and wiki_u > schwelle_fuer_null_vergleich) or \ - (wiki_u == 0 and crm_u > schwelle_fuer_null_vergleich) or \ - (crm_u > 0 and wiki_u > 0 and max(crm_u, wiki_u) > 0 and (abs(crm_u - wiki_u) / max(crm_u, wiki_u)) > abweichung_prozent_config): - results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT" - results["begruendungen"].append(f"Umsatz CRM ({crm_u:,.0f}) vs. Wiki ({wiki_u:,.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab oder einer ist 0.") - else: - results["abweichung_umsatz_flag"] = "OK" - else: # Sollte nicht passieren, wenn _get_numeric_value_for_plausi nur >=0 oder NaN liefert - results["abweichung_umsatz_flag"] = "INFO_NEGATIV_UNBEKANNT" - elif pd.isna(crm_u) and not pd.isna(wiki_u): results["abweichung_umsatz_flag"] = "CRM_FEHLT" - elif not pd.isna(crm_u) and pd.isna(wiki_u): results["abweichung_umsatz_flag"] = "WIKI_FEHLT" - # Wenn beide NaN sind, bleibt es "N/A" - - # Mitarbeiter Abweichung - if not pd.isna(crm_m) and not pd.isna(wiki_m): - if crm_m == 0 and wiki_m == 0: - results["abweichung_ma_flag"] = "OK_BEIDE_NULL" - elif crm_m >= 0 and wiki_m >= 0: - schwelle_ma_fuer_null_vergleich = getattr(Config, 'PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ', 3) # Kleine Zahl - if (crm_m == 0 and wiki_m > schwelle_ma_fuer_null_vergleich) or \ - (wiki_m == 0 and crm_m > schwelle_ma_fuer_null_vergleich) or \ - (crm_m > 0 and wiki_m > 0 and max(crm_m,wiki_m) > 0 and (abs(crm_m - wiki_m) / max(crm_m, wiki_m)) > abweichung_prozent_config): - results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT" - results["begruendungen"].append(f"MA CRM ({crm_m:.0f}) vs. Wiki ({wiki_m:.0f}) weicht signifikant ab oder einer ist 0.") - else: - results["abweichung_ma_flag"] = "OK" + # Umsatz Abweichung (arbeitet mit Mio-Werten) + if crm_u_mio > 0 and wiki_u_mio > 0: # Beide müssen positive Werte haben für sinnvollen Vergleich + diff = abs(crm_u_mio - wiki_u_mio) + max_val = max(crm_u_mio, wiki_u_mio) + if (diff / max_val) > abweichung_prozent_config: + results["abweichung_umsatz_flag"] = "WARNUNG_SIGNIFIKANT" + results["begruendungen"].append(f"Umsatz CRM ({crm_u_mio:,.1f} Mio) vs. Wiki ({wiki_u_mio:,.1f} Mio) weicht >{abweichung_prozent_config*100:.0f}% ab.") else: - results["abweichung_ma_flag"] = "INFO_NEGATIV_UNBEKANNT" - elif pd.isna(crm_m) and not pd.isna(wiki_m): results["abweichung_ma_flag"] = "CRM_FEHLT" - elif not pd.isna(crm_m) and pd.isna(wiki_m): results["abweichung_ma_flag"] = "WIKI_FEHLT" - # Wenn beide NaN sind, bleibt es "N/A" + results["abweichung_umsatz_flag"] = "OK" + elif crm_u_mio > 0 and wiki_u_mio <= 0 : results["abweichung_umsatz_flag"] = "WIKI_FEHLT_ODER_NULL" + elif crm_u_mio <= 0 and wiki_u_mio > 0 : results["abweichung_umsatz_flag"] = "CRM_FEHLT_ODER_NULL" + else: results["abweichung_umsatz_flag"] = "BEIDE_FEHLEN_ODER_NULL" # Oder N/A + + # Mitarbeiter Abweichung (arbeitet mit absoluten Zahlen) + if crm_m_abs > 0 and wiki_m_abs > 0: + diff = abs(crm_m_abs - wiki_m_abs) + max_val = max(crm_m_abs, wiki_m_abs) + if (diff / max_val) > abweichung_prozent_config: + results["abweichung_ma_flag"] = "WARNUNG_SIGNIFIKANT" + results["begruendungen"].append(f"MA CRM ({crm_m_abs:.0f}) vs. Wiki ({wiki_m_abs:.0f}) weicht >{abweichung_prozent_config*100:.0f}% ab.") + else: + results["abweichung_ma_flag"] = "OK" + elif crm_m_abs > 0 and wiki_m_abs <= 0 : results["abweichung_ma_flag"] = "WIKI_FEHLT_ODER_NULL" + elif crm_m_abs <= 0 and wiki_m_abs > 0 : results["abweichung_ma_flag"] = "CRM_FEHLT_ODER_NULL" + else: results["abweichung_ma_flag"] = "BEIDE_FEHLEN_ODER_NULL" - if not results["begruendungen"]: - results["plausi_begruendung_final"] = "Plausibilität OK" - else: - results["plausi_begruendung_final"] = "; ".join(results["begruendungen"]) + if not results["begruendungen"]: results["plausi_begruendung_final"] = "Plausibilität OK" + else: results["plausi_begruendung_final"] = "; ".join(results["begruendungen"]) return results + def run_plausibility_checks_batch(self, start_sheet_row=None, end_sheet_row=None, limit=None): self.logger.info(f"Starte Modus 'Plausibilitäts-Checks mit Konsolidierung'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Datenstart'} bis {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}") plausi_ts_key = "Plausibilität Prüfdatum" - if plausi_ts_key not in COLUMN_MAP: - self.logger.error(f"FEHLER: Schlüssel '{plausi_ts_key}' nicht in COLUMN_MAP. Abbruch.") + # Stellen Sie sicher, dass alle benötigten Spalten in COLUMN_MAP existieren + required_keys_for_mode = [ + "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", # Input für Konsolidierung + "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)", # Output Konsolidierung / Input Plausi + "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", # Output Plausi + "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", # Output Plausi + plausi_ts_key # Output Timestamp + ] + if not all(key in COLUMN_MAP for key in required_keys_for_mode): + missing_k = [k for k in required_keys_for_mode if k not in COLUMN_MAP] + self.logger.error(f"Nicht alle benötigten Spalten ({missing_k}) für Plausi-Checks mit Konsolidierung in COLUMN_MAP. Abbruch.") return if not self.sheet_handler.load_data(): @@ -8171,27 +8139,6 @@ class DataProcessor: all_data = self.sheet_handler.get_all_data_with_headers() header_offset = self.sheet_handler._header_rows - # Spalten für Konsolidierungs-Input - kons_input_keys = [ - "CRM Umsatz", "Wiki Umsatz", "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter" - ] - # Spalten für Konsolidierungs-Output (und Plausi-Input) - kons_output_keys = [ - "Finaler Umsatz (Wiki>CRM)", "Finaler Mitarbeiter (Wiki>CRM)" - ] - # Spalten für Plausi-Output - plausi_output_keys = [ - "Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", - "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki", "Plausibilität Begründung", - plausi_ts_key # Inklusive des neuen Timestamps - ] - - all_needed_keys = kons_input_keys + kons_output_keys + plausi_output_keys - if not all(key in COLUMN_MAP for key in all_needed_keys): - missing_k = [k for k in all_needed_keys if k not in COLUMN_MAP] - self.logger.error(f"Nicht alle benötigten Spalten ({missing_k}) für Plausi-Checks mit Konsolidierung in COLUMN_MAP. Abbruch.") - return - updates_fuer_sheet = [] processed_rows_count = 0 now_timestamp_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -8199,112 +8146,117 @@ class DataProcessor: effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1 effective_end_row = end_sheet_row if end_sheet_row is not None else len(all_data) - self.logger.info(f"Prüfe und konsolidiere Zeilen {effective_start_row} bis {effective_end_row} für Plausi-Checks.") + self.logger.info(f"Konsolidiere und prüfe Plausibilität für Zeilen {effective_start_row} bis {effective_end_row}.") for row_num_sheet in range(effective_start_row, effective_end_row + 1): if limit is not None and processed_rows_count >= limit: - self.logger.info(f"Limit von {limit} Zeilen für Plausi-Checks erreicht.") + self.logger.info(f"Verarbeitungslimit von {limit} Zeilen erreicht.") break row_list_idx = row_num_sheet - 1 if row_list_idx >= len(all_data): break row_data = all_data[row_list_idx] + + # CRM Name für Logging + # crm_name_log = self._get_cell_value_safe(row_data, "CRM Name") + # self.logger.debug(f"Verarbeite Zeile {row_num_sheet} ({crm_name_log[:30]}...) für Plausi-Check & Konsolidierung.") + + current_row_updates = [] # --- 1. Konsolidierung (Logik aus _process_single_row, Block 3e) --- final_umsatz_str_konsolidiert = "k.A." final_ma_str_konsolidiert = "k.A." + crm_umsatz_val_str = self._get_cell_value_safe(row_data, "CRM Umsatz") - # Für Wiki-Werte: Wenn _process_single_row nicht lief, sind final_wiki_data nicht aktuell. - # Wir müssen die Wiki-Werte direkt aus row_data lesen für diesen Modus. - wiki_umsatz_val_str = self._get_cell_value_safe(row_data, "Wiki Umsatz") + wiki_umsatz_val_str = self._get_cell_value_safe(row_data, "Wiki Umsatz") # Liest direkt aus dem Sheet crm_ma_val_str = self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter") - wiki_ma_val_str = self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") + wiki_ma_val_str = self._get_cell_value_safe(row_data, "Wiki Mitarbeiter") # Liest direkt aus dem Sheet try: + # get_numeric_filter_value liefert Umsatz in Mio, MA absolut, oder 0 bei Fehler/unbekannt num_crm_umsatz = get_numeric_filter_value(crm_umsatz_val_str, is_umsatz=True) num_wiki_umsatz = get_numeric_filter_value(wiki_umsatz_val_str, is_umsatz=True) num_crm_ma = get_numeric_filter_value(crm_ma_val_str, is_umsatz=False) num_wiki_ma = get_numeric_filter_value(wiki_ma_val_str, is_umsatz=False) - final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz > 0 else num_crm_umsatz - final_num_ma = num_wiki_ma if num_wiki_ma > 0 else num_crm_ma + # Konsolidierung: Wiki > CRM. Priorisiere den Wert, der nicht 0 ist (was "unbekannt" von get_numeric_filter_value bedeutet). + final_num_umsatz = num_wiki_umsatz if num_wiki_umsatz != 0.0 else num_crm_umsatz + final_num_ma = num_wiki_ma if num_wiki_ma != 0 else num_crm_ma - # String-Konvertierung (0 wird zu "k.A." wenn es aus leer/k.A. Quellen kommt, - # oder zu "0" wenn es z.B. aus "173" (Euro) gerundet wird) - final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) if final_num_umsatz > 0 or (final_num_umsatz == 0 and crm_umsatz_val_str == "0" and wiki_umsatz_val_str == "0" ) else 'k.A.' - final_ma_str_konsolidiert = str(int(round(final_num_ma))) if final_num_ma > 0 or (final_num_ma == 0 and crm_ma_val_str == "0" and wiki_ma_val_str == "0") else 'k.A.' + # String-Konvertierung für Sheet: + # Ein numerischer Wert von 0 (der aus "0 Tsd" oder echter 0 kommen kann) wird als String "0" geschrieben. + # Wenn final_num_xxx 0 ist, weil *beide* Quellen 0 (oder leer/k.A.) waren, wird "k.A." geschrieben. + if final_num_umsatz == 0.0 and (num_crm_umsatz == 0.0 and num_wiki_umsatz == 0.0): # Beide Quellen waren "unbekannt" + final_umsatz_str_konsolidiert = 'k.A.' + else: + final_umsatz_str_konsolidiert = str(int(round(final_num_umsatz))) + + if final_num_ma == 0 and (num_crm_ma == 0 and num_wiki_ma == 0): # Beide Quellen waren "unbekannt" + final_ma_str_konsolidiert = 'k.A.' + else: + final_ma_str_konsolidiert = str(int(round(final_num_ma))) + + self.logger.debug(f" Zeile {row_num_sheet}: Konsolidiert -> U: '{final_umsatz_str_konsolidiert}', MA: '{final_ma_str_konsolidiert}'") except Exception as e_conso_direct: self.logger.error(f"Fehler bei direkter Konsolidierung in Plausi-Check für Zeile {row_num_sheet}: {e_conso_direct}") - final_umsatz_str_konsolidiert = "FEHLER_KONSO_DIREKT" - final_ma_str_konsolidiert = "FEHLER_KONSO_DIREKT" + final_umsatz_str_konsolidiert = "FEHLER_KONSO_IN_PLAUSI" + final_ma_str_konsolidiert = "FEHLER_KONSO_IN_PLAUSI" - # Update-Dicts für die konsolidierten Werte vorbereiten - current_row_updates = [] current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Umsatz (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_umsatz_str_konsolidiert]]}) current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Finaler Mitarbeiter (Wiki>CRM)"] + 1)}{row_num_sheet}', 'values': [[final_ma_str_konsolidiert]]}) - # --- 2. Plausibilitäts-Checks (basierend auf den gerade konsolidierten Werten) --- - # Bedingung, um Plausi-Check überhaupt zu starten (Input-Werte müssen vorhanden sein) - valid_input_for_plausi_check = True - if final_umsatz_str_konsolidiert.upper().startswith("FEHLER") or final_umsatz_str_konsolidiert.lower() == 'k.a.' or \ - final_ma_str_konsolidiert.upper().startswith("FEHLER") or final_ma_str_konsolidiert.lower() == 'k.a.': - # Wenn einer der Hauptwerte schon "k.A." oder Fehler ist, sind viele Plausi-Checks nicht sinnvoll - # Wir setzen die Flags auf NICHT_PRUEFBAR oder spezifische Fehler und die Begründung - plausi_results = { - "plaus_umsatz_flag": "NICHT_PRUEFBAR" if final_umsatz_str_konsolidiert.lower() == 'k.a.' else "FEHLER_INPUT", - "plaus_ma_flag": "NICHT_PRUEFBAR" if final_ma_str_konsolidiert.lower() == 'k.a.' else "FEHLER_INPUT", - "plaus_ratio_flag": "NICHT_PRUEFBAR", - "abweichung_umsatz_flag": "N/A", - "abweichung_ma_flag": "N/A", - "plausi_begruendung_final": f"Input für Plausi-Check unvollständig/fehlerhaft (U: {final_umsatz_str_konsolidiert}, MA: {final_ma_str_konsolidiert})." - } - valid_input_for_plausi_check = False # Für das Zählen - - if valid_input_for_plausi_check: - processed_rows_count +=1 # Zähle nur, wenn Plausi-Check tatsächlich durchgeführt wird + # Nur ausführen, wenn Konsolidierung keine FEHLER produziert hat + if not final_umsatz_str_konsolidiert.startswith("FEHLER") and not final_ma_str_konsolidiert.startswith("FEHLER"): + processed_rows_count +=1 # Zähle nur Zeilen, für die Plausi tatsächlich versucht wird try: plausi_input_data = { "Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, "Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, - "CRM Umsatz": crm_umsatz_val_str, # Originalwerte für Abweichungscheck + "CRM Umsatz": crm_umsatz_val_str, "Wiki Umsatz": wiki_umsatz_val_str, "CRM Anzahl Mitarbeiter": crm_ma_val_str, "Wiki Mitarbeiter": wiki_ma_val_str } plausi_results = self._check_financial_plausibility(plausi_input_data) + + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "ERR_FLAG")]]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) + except Exception as e_plausi_run: - self.logger.error(f"Fehler im Plausi-Check Lauf für Zeile {row_num_sheet}: {e_plausi_run}") - plausi_results = { # Fehler-Defaults - "plaus_umsatz_flag": "FEHLER_CHECK_RUNTIME", "plaus_ma_flag": "FEHLER_CHECK_RUNTIME", - "plaus_ratio_flag": "FEHLER_CHECK_RUNTIME", "abweichung_umsatz_flag": "FEHLER_CHECK_RUNTIME", - "abweichung_ma_flag": "FEHLER_CHECK_RUNTIME", - "plausi_begruendung_final": f"Systemfehler Plausi-Check: {str(e_plausi_run)[:100]}" - } - - # Plausi-Ergebnisse zu Updates hinzufügen - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung Umsatz CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_umsatz_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Abweichung MA CRM/Wiki"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("abweichung_ma_flag", "ERR_FLAG")]]}) - current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plausi_begruendung_final", "Fehler Begr.")]]}) + self.logger.error(f"Fehler im Plausi-Check Aufruf für Zeile {row_num_sheet}: {e_plausi_run}") + for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['FEHLER_PLAUSI_CALL']]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [[f"Systemfehler Plausi-Call: {str(e_plausi_run)[:100]}"]]}) + else: # Konsolidierung hat Fehler produziert + self.logger.warn(f"Zeile {row_num_sheet}: Überspringe Plausi-Checks wegen Fehler bei Konsolidierung.") + # Setze Plausi-Flags auf einen Fehlerstatus oder lasse sie leer (aktuell wird nichts explizit gesetzt) + for key_flag in ["Plausibilität Umsatz", "Plausibilität Mitarbeiter", "Plausibilität Umsatz/MA Ratio", "Abweichung Umsatz CRM/Wiki", "Abweichung MA CRM/Wiki"]: + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[key_flag] + 1)}{row_num_sheet}', 'values': [['INPUT_FEHLER_KONSO']]}) + current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Begründung"] + 1)}{row_num_sheet}', 'values': [["Konsolidierung fehlgeschlagen"]]}) + + + # Immer den Plausi-Timestamp setzen, da die Zeile betrachtet wurde current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP[plausi_ts_key] + 1)}{row_num_sheet}', 'values': [[now_timestamp_str]]}) - updates_fuer_sheet.extend(current_row_updates) - # Batch-Update Logik (Anzahl Spalten: 2 Konsolidierung + 6 Plausi + 1 TS = 9) + # Batch-Update Logik (Anzahl Spalten jetzt: 2 Konsolidierung + 6 Plausi-Flags/Begründung + 1 Plausi-TS = 9) if len(updates_fuer_sheet) >= getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 9: self.logger.info(f"Sende Plausi-Check & Konsolidierungs Batch-Update ({len(updates_fuer_sheet)//9} Zeilen)...") self.sheet_handler.batch_update_cells(updates_fuer_sheet) updates_fuer_sheet = [] + # time.sleep(0.5) # Kleine optionale Pause if updates_fuer_sheet: self.logger.info(f"Sende finalen Plausi-Check & Konsolidierungs Batch-Update ({len(updates_fuer_sheet)//9} Zeilen)...") self.sheet_handler.batch_update_cells(updates_fuer_sheet) - self.logger.info(f"Plausibilitäts-Check-Lauf (mit Konsolidierung) beendet. {processed_rows_count} Zeilen geprüft/konsolidiert.") + self.logger.info(f"Plausibilitäts-Check-Lauf (mit Konsolidierung) beendet. {processed_rows_count} Zeilen mit Plausi-Checks versehen, {skipped_count} Zeilen initial übersprungen.") # ========================================================================== # === Utility Methods (ML Data Prep & Training) ============================