diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 1a66d29a..306162ed 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 """ -Version: v1.6.1 (Refactored) -Datum: {aktuelles Datum} -Git-Überschrift (max. 100 Zeichen): -v1.6.1: Verbessere Website-Scraping zur Umgehung von Cookie-Bannern +v1.6.2: Verfeinere Timestamp-Logik & integriere ML-Datenvorbereitung Git-Änderungsbeschreibung: -- Überarbeite `get_website_raw` zur besseren Handhabung von Cookie-Bannern. -- Priorisiere Scraping von Hauptinhalt-Tags (`
`, `
`, spezifische IDs/Klassen). -- Implementiere Fallback auf `` mit Versuch, häufige Banner-Elemente zu entfernen (`.decompose()`). -- Füge Heuristik hinzu, um extrahierten Text zu verwerfen, wenn er wahrscheinlich nur Banner-Inhalt ist. -- Erhöhe Request-Timeout in `get_website_raw` leicht auf 15 Sekunden. +- Passe Dispatcher (`run_dispatcher`) und `GoogleSheetHandler.get_start_row_index` an, um den Startpunkt basierend auf dem Website Scrape Timestamp (Spalte AT) zu bestimmen. +- Implementiere individuelle Timestamp-Prüfungen in den Batch-Funktionen (`process_verification_only` (AN), `process_website_batch` (AT), `process_branch_batch` (AO)), um das erneute Verarbeiten abgeschlossener Zeilen zu verhindern. +- Überarbeite `_process_single_row` (`full_run`, `reeval`), um für jeden Teilbereich (Wiki, Website, Chat) den zugehörigen Timestamp zu prüfen und nur bei Bedarf auszuführen. +- Passe `_process_batch` an, sodass es nur noch Ergebnisspalten (S-Y) schreibt; Timestamps werden jetzt von der aufrufenden Funktion gesetzt. +- Füge neue Spalten (AT: Website TS, AU: Gesch. Techniker Bucket, AV: Finaler Umsatz, AW: Finaler MA) zur `alignment_demo` und `COLUMN_MAP` hinzu. +- Integriere die Funktion `prepare_data_for_modeling` als Methode in die `DataProcessor`-Klasse (wird noch nicht aktiv in einem Modus aufgerufen). """ import os @@ -54,7 +52,7 @@ LOG_DIR = "Log" # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.6.1" + VERSION = "v1.6.2" LANG = "de" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" MAX_RETRIES = 3 @@ -98,52 +96,58 @@ ALLOWED_TARGET_BRANCHES = [] # Globales Spalten-Mapping (Beispiel basierend auf Zeile 4 - Kurze Beschreibung) # TODO: Dieses Mapping vervollständigen und durchgängig verwenden! COLUMN_MAP = { - "ReEval Flag": 0, - "CRM Name": 1, - "CRM Kurzform": 2, - "CRM Website": 3, - "CRM Ort": 4, - "CRM Beschreibung": 5, # Index 5, nicht 6 - "CRM Branche": 6, # Index 6 - "CRM Beschreibung Branche extern": 7, # Index 7 - "CRM Anzahl Techniker": 8, # Index 8 - "CRM Umsatz": 9, # Index 9 - "CRM Anzahl Mitarbeiter": 10, # Index 10 - "CRM Vorschlag Wiki URL": 11, # Index 11 - "Wiki URL": 12, # Index 12 - "Wiki Absatz": 13, # Index 13 - "Wiki Branche": 14, # Index 14 - "Wiki Umsatz": 15, # Index 15 - "Wiki Mitarbeiter": 16, # Index 16 - "Wiki Kategorien": 17, # Index 17 + "ReEval Flag": 0, # A + "CRM Name": 1, # B + "CRM Kurzform": 2, # C + "CRM Website": 3, # D + "CRM Ort": 4, # E + "CRM Beschreibung": 5, # F + "CRM Branche": 6, # G + "CRM Beschreibung Branche extern": 7, # H + "CRM Anzahl Techniker": 8, # I - !! Wichtig für die Zielvariable !! + "CRM Umsatz": 9, # J + "CRM Anzahl Mitarbeiter": 10, # K + "CRM Vorschlag Wiki URL": 11, # L + "Wiki URL": 12, # M + "Wiki Absatz": 13, # N + "Wiki Branche": 14, # O + "Wiki Umsatz": 15, # P + "Wiki Mitarbeiter": 16, # Q + "Wiki Kategorien": 17, # R "Chat Wiki Konsistenzprüfung": 18, # S "Chat Begründung Wiki Inkonsistenz": 19, # T "Chat Vorschlag Wiki Artikel": 20, # U "Begründung bei Abweichung": 21, # V (ungenutzt?) - "Chat Vorschlag Branche": 22, # W - "Chat Konsistenz Branche": 23, # X + "Chat Vorschlag Branche": 22, # W + "Chat Konsistenz Branche": 23, # X "Chat Begründung Abweichung Branche": 24, # Y "Chat Prüfung FSM Relevanz": 25, # Z "Chat Begründung für FSM Relevanz": 26, # AA "Chat Schätzung Anzahl Mitarbeiter": 27, # AB "Chat Konsistenzprüfung Mitarbeiterzahl": 28, # AC "Chat Begründung Abweichung Mitarbeiterzahl": 29, # AD - "Chat Einschätzung Anzahl Servicetechniker": 30, # AE + "Chat Einschätzung Anzahl Servicetechniker": 30, # AE (War das nicht die Schätzung? Verwechselt mit AU?) "Chat Begründung Abweichung Anzahl Servicetechniker": 31, # AF - "Chat Schätzung Umsatz": 32, # AG + "Chat Schätzung Umsatz": 32, # AG "Chat Begründung Abweichung Umsatz": 33, # AH "Linked Serviceleiter gefunden": 34, # AI "Linked It-Leiter gefunden": 35, # AJ "Linked Management gefunden": 36, # AK "Linked Disponent gefunden": 37, # AL "Contact Search Timestamp": 38, # AM - "Wikipedia Timestamp": 39, # AN + "Wikipedia Timestamp": 39, # AN "Timestamp letzte Prüfung": 40, # AO - "Version": 41, # AP - "Tokens": 42, # AQ - "Website Rohtext": 43, # AR - "Website Zusammenfassung": 44 # AS + "Version": 41, # AP + "Tokens": 42, # AQ + "Website Rohtext": 43, # AR + "Website Zusammenfassung": 44, # AS + # --- NEUE SPALTEN --- + "Website Scrape Timestamp": 45, # AT + "Geschätzter Techniker Bucket": 46, # AU + "Finaler Umsatz (Wiki>CRM)": 47,# AV + "Finaler Mitarbeiter (Wiki>CRM)": 48 # AW } +# Hinweis: Index ist 0-basiert, Spaltenbuchstaben sind 1-basiert (A=1, AW=49) # Annahme: COLUMN_MAP ist global definiert und enthält mindestens: # "CRM Name", "CRM Branche", "CRM Umsatz", "Wiki Umsatz", @@ -989,28 +993,106 @@ class GoogleSheetHandler: """Gibt alle Daten inklusive Header zurück.""" return self.sheet_values - def get_start_row_index(self, timestamp_col_index=COLUMN_MAP["Timestamp letzte Prüfung"]): + def get_start_row_index(self, check_column_index=COLUMN_MAP["Website Scrape Timestamp"], min_sheet_row=7): """ Findet den Index der ersten Zeile (0-basiert für Daten nach Header), - in der der Timestamp fehlt. Startet Suche ab Zeile 7 (Index 2 der Datenliste). + ab einer Mindestzeilennummer im Sheet, in der der Timestamp in der + angegebenen Spalte fehlt. + + Args: + check_column_index (int): Der 0-basierte Index der zu prüfenden Spalte. + min_sheet_row (int): Die 1-basierte Zeilennummer im Sheet, ab der gesucht werden soll. + + Returns: + int: Der 0-basierte Index in der Datenliste (ohne Header), + oder der Index nach der letzten Zeile, wenn alle gefüllt sind. """ - header_rows = 5 # Annahme: Zeile 1-5 sind Header - data_rows = self.sheet_values[header_rows:] + header_rows = 5 # Annahme: Zeilen 1-5 sind Header + data_rows = self.get_data() # Holt Daten ohne Header - # Startet die Suche ab der 7. Zeile des Sheets, was der 2. Datenzeile entspricht (Index 1) - search_start_index = max(0, 7 - header_rows -1) # Index bezogen auf data_rows + if not data_rows: + debug_print("Keine Datenzeilen vorhanden.") + return 0 - for i, row in enumerate(data_rows[search_start_index:], start=search_start_index): - if len(row) <= timestamp_col_index or not row[timestamp_col_index].strip(): + # Berechne den 0-basierten Startindex für die *Datenliste*, + # der der min_sheet_row entspricht. + search_start_index_in_data = max(0, min_sheet_row - header_rows - 1) + + for i, row in enumerate(data_rows[search_start_index_in_data:], start=search_start_index_in_data): + if len(row) <= check_column_index or not row[check_column_index].strip(): actual_sheet_row = i + header_rows + 1 # 1-basierte Zeilennummer im Sheet - debug_print(f"Erste Zeile ohne Zeitstempel in Spalte {timestamp_col_index+1} gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") + # Finde den Spaltenbuchstaben für die Log-Ausgabe + col_letter = self._get_col_letter(check_column_index + 1) + debug_print(f"Erste Zeile ab Zeile {min_sheet_row} ohne Zeitstempel in Spalte {col_letter} (Index {check_column_index}) gefunden: Zeile {actual_sheet_row} (Daten-Index {i})") return i # Gibt den 0-basierten Index *innerhalb der Datenliste* zurück - - # Wenn alle Zeilen ab Zeile 7 einen Zeitstempel haben + + # Wenn alle Zeilen ab min_sheet_row einen Zeitstempel haben last_index = len(data_rows) - debug_print(f"Alle Zeilen ab Zeile 7 haben einen Zeitstempel. Nächster Index wäre {last_index}.") + col_letter = self._get_col_letter(check_column_index + 1) + debug_print(f"Alle Zeilen ab Zeile {min_sheet_row} haben einen Zeitstempel in Spalte {col_letter}. Nächster Daten-Index wäre {last_index}.") return last_index # Gibt den Index nach der letzten Datenzeile zurück + # Hilfsfunktion zur Umwandlung von Spaltenindex in Buchstaben (für Logs) + def _get_col_letter(self, col_idx): + """ Konvertiert 1-basierten Spaltenindex in Buchstaben (A, B, ..., Z, AA, ...). """ + string = "" + while col_idx > 0: + col_idx, remainder = divmod(col_idx - 1, 26) + string = chr(65 + remainder) + string + return string + +# Anpassung in run_dispatcher: Verwende die neue Methode mit Spalte AT +def run_dispatcher(mode, sheet_handler, row_limit=None): + """Wählt den passenden Batch-Prozess basierend auf dem Modus.""" + debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.") + + # Finde Startzeile basierend auf Timestamp in Spalte AT (Index 45) + # Verwende die neue Methode des Handlers + start_data_index = sheet_handler.get_start_row_index(check_column_index=COLUMN_MAP["Website Scrape Timestamp"], min_sheet_row=7) + header_rows = 5 + start_row_index_in_sheet = start_data_index + header_rows + 1 + + all_data = sheet_handler.get_all_data_with_headers() # Hole alle Daten + total_sheet_rows = len(all_data) + + if start_row_index_in_sheet > total_sheet_rows: + debug_print(f"Startzeile ({start_row_index_in_sheet}) liegt hinter der letzten Sheet-Zeile ({total_sheet_rows}). Dispatcher beendet.") + return + + # Bestimme Endzeile + if row_limit is not None and row_limit > 0: + # Berechne Endzeile basierend auf Startzeile und Limit + end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, total_sheet_rows) + else: + end_row_index_in_sheet = total_sheet_rows # Bis zum Ende des Sheets + + debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row_index_in_sheet}, bis Zeile {end_row_index_in_sheet}.") + + if start_row_index_in_sheet > end_row_index_in_sheet: + debug_print("Startzeile liegt nach Endzeile. Keine Verarbeitung.") + return + + # --- Modusausführung (bleibt gleich, ABER die aufgerufenen Funktionen müssen den Timestamp prüfen!) --- + if mode == "wiki": + # process_verification_only muss AN Timestamp prüfen + process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) + elif mode == "website": + # process_website_batch muss AT Timestamp prüfen + process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) + elif mode == "branch": + # process_branch_batch muss AO Timestamp prüfen + process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) + elif mode == "combined": + debug_print("--- Start Combined Mode: Wiki ---") + process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AN + debug_print("--- Start Combined Mode: Website ---") + process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AT + debug_print("--- Start Combined Mode: Branch ---") + process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet) # Prüft AO + debug_print("--- Combined Mode abgeschlossen ---") + else: + debug_print(f"Ungültiger Modus '{mode}' im Dispatcher.") + @retry_on_failure def batch_update_cells(self, update_data): """ @@ -1878,80 +1960,294 @@ def _process_batch(sheet, batches, row_numbers): time.sleep(Config.RETRY_DELAY) -def process_verification_only(sheet_handler, row_limit=None): - """Batch-Prozess nur für Wikipedia-Verifizierung (Modus 51, jetzt 'wiki' im Dispatcher).""" - debug_print("Starte Wikipedia-Verifizierungsmodus (Batch)...") - - all_data = sheet_handler.get_all_data_with_headers() # Hole alle Daten inkl. Header - header_rows = 5 # Zeilen 1-5 sind Header - - # Finde Startzeile (erste Zeile ab Zeile 7 ohne Zeitstempel in AO) - start_row_index_in_sheet = -1 # 1-basierter Index im Sheet - for i in range(header_rows + 1, len(all_data) + 1): # Starte Prüfung ab Zeile 6 (Index 5) - if i < 7: continue # Überspringe Zeilen vor 7 - - row_index_in_list = i - 1 # 0-basierter Index in all_data - row = all_data[row_index_in_list] - # Prüfe Zeitstempel in Spalte AO (Index 40) - if len(row) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row[COLUMN_MAP["Timestamp letzte Prüfung"]].strip(): - start_row_index_in_sheet = i - break - - if start_row_index_in_sheet == -1: - debug_print("Keine Zeile ohne Zeitstempel in Spalte AO (ab Zeile 7) gefunden. Verifizierung übersprungen.") - return +def process_verification_only(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """ + Batch-Prozess nur für Wikipedia-Verifizierung. + Prüft für jede Zeile im Bereich, ob Timestamp AN bereits gesetzt ist. + """ + debug_print(f"Starte Wikipedia-Verifizierungsmodus (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") - debug_print(f"Verarbeitung startet ab Zeile {start_row_index_in_sheet} (erste Zeile ab 7 ohne Zeitstempel in AO).") - - # Bestimme Endzeile basierend auf row_limit - if row_limit is not None and row_limit > 0: - end_row_index_in_sheet = min(start_row_index_in_sheet + row_limit - 1, len(all_data)) - else: - end_row_index_in_sheet = len(all_data) # Bis zum Ende des Sheets - - if start_row_index_in_sheet > end_row_index_in_sheet: - debug_print("Startzeile liegt nach der Endzeile. Keine Verarbeitung.") - return - - debug_print(f"Verarbeite Zeilen von {start_row_index_in_sheet} bis {end_row_index_in_sheet}.") + all_data = sheet_handler.get_all_data_with_headers() + timestamp_col_index = COLUMN_MAP["Wikipedia Timestamp"] # Prüfe Spalte AN batch_size = Config.BATCH_SIZE current_batch = [] current_row_numbers = [] + processed_count = 0 + skipped_count = 0 for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): row_index_in_list = i - 1 + if row_index_in_list >= len(all_data): # Sicherheitscheck + debug_print(f"Warnung: Zeilenindex {row_index_in_list} außerhalb des Datenbereichs ({len(all_data)} Zeilen).") + continue + row = all_data[row_index_in_list] - - # Erstelle Text für den Prompt (verwende Spaltennamen/Indizes) - # Annahme: COLUMN_MAP ist verfügbar und korrekt + + # --- NEU: Timestamp-Prüfung für jede Zeile --- + if len(row) > timestamp_col_index and row[timestamp_col_index].strip(): + debug_print(f"Zeile {i}: Überspringe Wiki-Verifizierung (Timestamp AN bereits vorhanden: '{row[timestamp_col_index]}').") + skipped_count += 1 + continue + # --- Ende Timestamp-Prüfung --- + + # Erstelle Text für den Prompt (wie zuvor) company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else '' crm_desc = row[COLUMN_MAP["CRM Beschreibung"]] if len(row) > COLUMN_MAP["CRM Beschreibung"] else '' wiki_url = row[COLUMN_MAP["Wiki URL"]] if len(row) > COLUMN_MAP["Wiki URL"] and row[COLUMN_MAP["Wiki URL"]].strip() not in ['', 'k.A.'] else 'k.A.' wiki_paragraph = row[COLUMN_MAP["Wiki Absatz"]] if len(row) > COLUMN_MAP["Wiki Absatz"] else 'k.A.' wiki_categories = row[COLUMN_MAP["Wiki Kategorien"]] if len(row) > COLUMN_MAP["Wiki Kategorien"] else 'k.A.' - + entry_text = ( f"Eintrag {i}:\n" f" Firmenname: {company_name}\n" - f" CRM-Beschreibung: {crm_desc[:200]}...\n" # Gekürzt + f" CRM-Beschreibung: {crm_desc[:200]}...\n" f" Wikipedia-URL: {wiki_url}\n" - f" Wiki-Absatz: {wiki_paragraph[:200]}...\n" # Gekürzt - f" Wiki-Kategorien: {wiki_categories[:200]}...\n" # Gekürzt + f" Wiki-Absatz: {wiki_paragraph[:200]}...\n" + f" Wiki-Kategorien: {wiki_categories[:200]}...\n" f"----\n" ) current_batch.append(entry_text) current_row_numbers.append(i) + processed_count += 1 # Wenn Batch voll oder letzte Zeile erreicht if len(current_batch) == batch_size or i == end_row_index_in_sheet: - _process_batch(sheet_handler.sheet, current_batch, current_row_numbers) + if current_batch: # Nur wenn etwas im Batch ist + # _process_batch schreibt S-Y, AO, AP. AN wird *nicht* von _process_batch geschrieben! + # Wir müssen AN separat setzen oder _process_batch anpassen. + # Einfacher: Setzen AN hier *vor* dem Aufruf für die bearbeiteten Zeilen. + + # Erstelle Updates für den AN-Timestamp + wiki_ts_updates = [] + current_wiki_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + for row_num in current_row_numbers: + wiki_ts_updates.append({'range': f'AN{row_num}', 'values': [[current_wiki_timestamp]]}) + + # Setze zuerst den AN Timestamp + if wiki_ts_updates: + sheet_handler.batch_update_cells(wiki_ts_updates) + debug_print(f"Wiki-Timestamp AN für Batch {current_row_numbers[0]}-{current_row_numbers[-1]} gesetzt.") + + # Rufe dann _process_batch auf, das S-Y, AO, AP schreibt + _process_batch(sheet_handler.sheet, current_batch, current_row_numbers) # Nutzt noch alten Timestamp AO! Das müssen wir ändern. + # Reset für nächsten Batch current_batch = [] current_row_numbers = [] - debug_print("Wikipedia-Verifizierungs-Batch abgeschlossen.") + debug_print(f"Wikipedia-Verifizierungs-Batch abgeschlossen. {processed_count} Zeilen zur Verarbeitung an ChatGPT gesendet, {skipped_count} Zeilen übersprungen.") +# Anpassung in _process_batch: Setzt jetzt *nicht* mehr AO/AP, sondern nur S-Y +def _process_batch(sheet, batches, row_numbers): + """ + Hilfsfunktion für process_verification_only: Verarbeitet einen Batch von Wikipedia-Verifizierungsanfragen. + Aktualisiert NUR die Spalten S bis Y. Zeitstempel werden von der aufrufenden Funktion gesetzt. + """ + if not batches: return + # (Prompt Erstellung wie gehabt) + aggregated_prompt = ( + "Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. " + "Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel zum Firmennamen und zur Beschreibung passt. " + "Gib das Ergebnis für jeden Eintrag ausschließlich im folgenden Format auf einer neuen Zeile aus:\n" + "Eintrag : \n\n" + "Mögliche Antworten:\n" + "- 'OK' (wenn der Artikel gut passt)\n" + "- 'X | Alternativer Artikel: | Begründung: ' (wenn der Artikel nicht passt, aber ein besserer gefunden wurde)\n" + "- 'X | Kein passender Artikel gefunden | Begründung: ' (wenn der Artikel nicht passt und kein besserer gefunden wurde)\n" + "- 'Kein Wikipedia-Eintrag vorhanden.' (wenn initial keine URL angegeben wurde und keine Suche erfolgreich war - dieser Fall sollte selten sein, da die Suche vorher stattfindet)\n\n" + "Einträge:\n" + "----------\n" + ) + aggregated_prompt += "".join(batches) + aggregated_prompt += "----------\nBitte nur die 'Eintrag X: Antwort'-Zeilen ausgeben." + + debug_print(f"Verarbeite Verifizierungs-Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.") + prompt_tokens = token_count(aggregated_prompt) + debug_print(f"Token-Zahl für Verifizierungs-Batch: {prompt_tokens}") + + chat_response = call_openai_chat(aggregated_prompt, temperature=0.0) + + if not chat_response: + debug_print(f"Fehler: Keine Antwort von OpenAI für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]}.") + return + + # Parse die aggregierte Antwort (wie gehabt) + answers = {} + lines = chat_response.strip().split('\n') + for line in lines: + match = re.match(r"Eintrag (\d+): (.*)", line.strip()) + if match: + row_num = int(match.group(1)) + answer_text = match.group(2).strip() + if row_num in row_numbers: answers[row_num] = answer_text + + # Bereite Batch-Update nur für Spalten S-Y vor + updates = [] + # current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Nicht mehr hier + # current_version = Config.VERSION # Nicht mehr hier + + for row_num in row_numbers: + answer = answers.get(row_num, "k.A. (Keine Antwort im Batch)") + # debug_print(f"Zeile {row_num} Verifizierungsantwort: '{answer}'") # Optional weniger Lärm + + wiki_confirm, alt_article, wiki_explanation = "", "", "" + v_val, w_val, x_val, y_val = "", "", "", "" + + if answer.upper() == "OK": wiki_confirm = "OK" + elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.": + wiki_confirm, alt_article, wiki_explanation = "X", "Kein Wikipedia-Eintrag vorhanden.", "Ursprünglich keine URL oder Suche erfolglos." + elif answer.startswith("X |"): + parts = answer.split("|", 2) + wiki_confirm = "X" + if len(parts) > 1: + detail = parts[1].strip() + if detail.startswith("Alternativer Artikel:"): alt_article = detail.split(":", 1)[1].strip() + elif detail == "Kein passender Artikel gefunden": alt_article = detail + else: alt_article = detail + if len(parts) > 2: + reason_part = parts[2].strip() + if reason_part.startswith("Begründung:"): wiki_explanation = reason_part.split(":", 1)[1].strip() + else: wiki_explanation = reason_part + else: + wiki_confirm, wiki_explanation = "?", f"Unerwartetes Format: {answer}" + + # Füge Updates für S-Y hinzu + updates.append({'range': f'S{row_num}', 'values': [[wiki_confirm]]}) + updates.append({'range': f'T{row_num}', 'values': [[alt_article]]}) + updates.append({'range': f'U{row_num}', 'values': [[wiki_explanation]]}) + updates.append({'range': f'V{row_num}:Y{row_num}', 'values': [[v_val, w_val, x_val, y_val]]}) + + # Führe das Batch-Update für S-Y durch + if updates: + # Direkten Sheet-Zugriff nutzen, da sheet übergeben wird + try: + sheet.batch_update(updates) + debug_print(f"Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} (S-Y) erfolgreich in Google Sheet aktualisiert.") + except Exception as e: + debug_print(f"FEHLER beim Batch-Update (S-Y) für Batch {row_numbers[0]}-{row_numbers[-1]}: {e}") + else: + debug_print(f"Keine Updates (S-Y) für Verifizierungs-Batch {row_numbers[0]}-{row_numbers[-1]} generiert.") + + # Kurze Pause nach jedem Batch-API-Call (jetzt in der aufrufenden Funktion) + # time.sleep(Config.RETRY_DELAY) # Entfernt + +# Komplette Funktion process_website_batch (prüft jetzt Timestamp AT) +def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """Batch-Prozess für Website-Scraping (Rohtext & Zusammenfassung).""" + debug_print(f"Starte Website-Scraping (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") + + all_data = sheet_handler.get_all_data_with_headers() + sheet = sheet_handler.sheet + timestamp_col_index = COLUMN_MAP["Website Scrape Timestamp"] # Prüfe Spalte AT + processed_count = 0 + skipped_count = 0 + + for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): + row_index_in_list = i - 1 + if row_index_in_list >= len(all_data): continue + row = all_data[row_index_in_list] + + # --- NEU: Timestamp-Prüfung für jede Zeile --- + if len(row) > timestamp_col_index and row[timestamp_col_index].strip(): + debug_print(f"Zeile {i}: Überspringe Website-Scraping (Timestamp AT bereits vorhanden: '{row[timestamp_col_index]}').") + skipped_count += 1 + continue + # --- Ende Timestamp-Prüfung --- + + website_url = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" + if not website_url or website_url.strip().lower() == "k.a.": + debug_print(f"Zeile {i}: Kein gültiger Website-Eintrag, überspringe Website-Scraping.") + # Optional: Zeitstempel trotzdem setzen als "geprüft, keine URL"? Eher nicht. + skipped_count += 1 # Zähle als übersprungen + continue + + debug_print(f"Zeile {i}: Verarbeite Website {website_url}...") + raw_text = get_website_raw(website_url) + summary = summarize_website_content(raw_text) # Braucht OpenAI Key + processed_count += 1 + + updates = [] + current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + current_version = Config.VERSION + + updates.append({'range': f'AR{i}', 'values': [[raw_text]]}) # Spalte AR + updates.append({'range': f'AS{i}', 'values': [[summary]]}) # Spalte AS + updates.append({'range': f'AT{i}', 'values': [[current_timestamp]]}) # Spalte AT (NEU) + updates.append({'range': f'AP{i}', 'values': [[current_version]]}) # Spalte AP (Version) + # AO (Letzte Prüfung) wird hier *nicht* gesetzt, das macht der Branch- oder Full-Run + + # Führe Batch-Update für diese eine Zeile durch + if updates: + sheet_handler.batch_update_cells(updates) + # Weniger Lärm im Log: debug_print(f"Zeile {i}: Website-Daten aktualisiert | Zeitstempel AT: {current_timestamp}, Version: {current_version}") + + # Pause zwischen den Zeilen/Websites + time.sleep(Config.RETRY_DELAY) + + debug_print(f"Website-Scraping (Batch) abgeschlossen. {processed_count} Websites gescraped, {skipped_count} Zeilen übersprungen.") + + +# Komplette Funktion process_branch_batch (prüft jetzt Timestamp AO) +def process_branch_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): + """Batch-Prozess für Brancheneinschätzung.""" + debug_print(f"Starte Brancheneinschätzung (Batch) für Zeilen {start_row_index_in_sheet} bis {end_row_index_in_sheet}...") + + all_data = sheet_handler.get_all_data_with_headers() + sheet = sheet_handler.sheet + timestamp_col_index = COLUMN_MAP["Timestamp letzte Prüfung"] # Prüfe Spalte AO + processed_count = 0 + skipped_count = 0 + + # Stelle sicher, dass das Branchenschema geladen ist + if not ALLOWED_TARGET_BRANCHES: + load_target_schema() # Versuch es zu laden + if not ALLOWED_TARGET_BRANCHES: + debug_print("FEHLER: Ziel-Branchenschema konnte nicht geladen werden. Breche Branch-Batch ab.") + return + + for i in range(start_row_index_in_sheet, end_row_index_in_sheet + 1): + row_index_in_list = i - 1 + if row_index_in_list >= len(all_data): continue + row = all_data[row_index_in_list] + + # --- NEU: Timestamp-Prüfung für jede Zeile --- + if len(row) > timestamp_col_index and row[timestamp_col_index].strip(): + debug_print(f"Zeile {i}: Überspringe Branchen-Einschätzung (Timestamp AO bereits vorhanden: '{row[timestamp_col_index]}').") + skipped_count += 1 + continue + # --- Ende Timestamp-Prüfung --- + + # Hole benötigte Daten aus der Zeile (wie zuvor) + crm_branche = row[COLUMN_MAP["CRM Branche"]] if len(row) > COLUMN_MAP["CRM Branche"] else "" + beschreibung = row[COLUMN_MAP["CRM Beschreibung"]] if len(row) > COLUMN_MAP["CRM Beschreibung"] else "" + wiki_branche = row[COLUMN_MAP["Wiki Branche"]] if len(row) > COLUMN_MAP["Wiki Branche"] else "" + wiki_kategorien = row[COLUMN_MAP["Wiki Kategorien"]] if len(row) > COLUMN_MAP["Wiki Kategorien"] else "" + website_summary = row[COLUMN_MAP["Website Zusammenfassung"]] if len(row) > COLUMN_MAP["Website Zusammenfassung"] else "" + + debug_print(f"Zeile {i}: Starte Brancheneinschätzung...") + result = evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary) + processed_count += 1 + + updates = [] + current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + current_version = Config.VERSION + + updates.append({'range': f'W{i}', 'values': [[result.get("branch", "Fehler")]]}) # Spalte W + updates.append({'range': f'X{i}', 'values': [[result.get("consistency", "Fehler")]]}) # Spalte X + updates.append({'range': f'Y{i}', 'values': [[result.get("justification", "Fehler")]]})# Spalte Y + updates.append({'range': f'AO{i}', 'values': [[current_timestamp]]}) # Spalte AO (Timestamp letzte Prüfung) + updates.append({'range': f'AP{i}', 'values': [[current_version]]}) # Spalte AP (Version) + + # Führe Batch-Update für diese eine Zeile durch + if updates: + sheet_handler.batch_update_cells(updates) + debug_print(f"Zeile {i}: Branch-Einschätzung aktualisiert: {result['branch']} ({result['consistency']}) | Zeitstempel AO: {current_timestamp}, Version: {current_version}") + + # Pause zwischen den API-Aufrufen + time.sleep(Config.RETRY_DELAY) + + debug_print(f"Brancheneinschätzung (Batch) abgeschlossen. {processed_count} Zeilen eingeschätzt, {skipped_count} Zeilen übersprungen.") def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index_in_sheet): """Batch-Prozess für Website-Scraping (Rohtext & Zusammenfassung).""" @@ -1987,7 +2283,7 @@ def process_website_batch(sheet_handler, start_row_index_in_sheet, end_row_index updates.append({'range': f'AR{i}', 'values': [[raw_text]]}) # Spalte AR updates.append({'range': f'AS{i}', 'values': [[summary]]}) # Spalte AS - updates.append({'range': f'AO{i}', 'values': [[current_timestamp]]}) # Spalte AO + updates.append({'range': f'AT{i}', 'values': [[current_timestamp]]}) # Spalte AT updates.append({'range': f'AP{i}', 'values': [[current_version]]}) # Spalte AP # Führe Batch-Update für diese eine Zeile durch @@ -2423,18 +2719,46 @@ def process_contact_research(sheet_handler): # Diese Funktion ist bereits im Code vorhanden (Zeile ~1230 in der vorherigen Version) # Sie bleibt unverändert: def alignment_demo(sheet): - """Schreibt die Header-Struktur (Zeilen 1-5) ins angegebene Sheet.""" - # Definition der Header wie im Original-Code + """Schreibt die Header-Struktur (Zeilen 1-5, jetzt bis Spalte AW) ins angegebene Sheet.""" new_headers = [ - ["ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", "Wiki URL", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Chat Wiki Konsistenzprüfung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Chat Vorschlag Branche", "Chat Konsistenz Branche", "Chat Begründung 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", "Contact Search Timestamp", "Wikipedia Timestamp", "Timestamp letzte Prüfung", "Version", "Tokens", "Website Rohtext", "Website Zusammenfassung"], - ["CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "System", "System", "System", "System", "System", "Web Scraper", "Chat GPT API"], - ["Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Beschreibung (Text)", "Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", "Wikipedia Artikel", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Verifizierung", "Begründung bei Abweichung", "Wikipedia Artikel", "Wikipedia Artikel", "Branche", "Branche", "Branche", "FSM Relevanz", "FSM Relevanz", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Servicetechniker", "Anzahl Servicetechniker", "Umsatz", "Umsatz", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Timestamp", "Timestamp", "Timestamp", "Version des Skripts die verwendet wurde", "ChatGPT Tokens", "Website-Content", "Website Zusammenfassung"], - ["Systemspalte, irrelevant für den Prompt. Wird zur manuellen Neuprüfung genutzt.", "Enthält den Firmennamen; Normalisierung erfolgt bei der Suche.", "Manuell gepflegte Kurzform, meist die ersten 2 Worte.", "Website des Unternehmens.", "Ort des Unternehmens.", "Kurze Beschreibung des Unternehmens.", "Aktuelle Branchenzuweisung gemäß Ziel-Branchenschema.", "Externe Branchenbeschreibung (z.B. von Dealfront).", "Recherchierte Anzahl Servicetechniker.", "Umsatz in Mio. € (CRM).", "Anzahl Mitarbeiter (CRM).", "Vorgeschlagene Wikipedia URL (Ausgangspunkt).", "Wikipedia URL (Ergebnis der Suche).", "Erster Absatz des Wikipedia-Artikels.", "Wikipedia-Branche – für den Branchenabgleich.", "Wikipedia-Umsatz – zur Validierung.", "Wikipedia-Mitarbeiterzahl – zur Validierung.", "Liste der Wikipedia-Kategorien.", "\"OK\" oder \"X\" – Ergebnis der Wikipedia-Validierung.", "Begründung bei Inkonsistenz (Wiki).", "Chat-Vorschlag Wiki Artikel: Falls kein passender Artikel gefunden, alternativ vorschlagen.", "Nicht genutzt, evtl. für zukünftige Funktionen.", "Branchenvorschlag via ChatGPT (alternativer Vorschlag).", "Vergleich: Übereinstimmung CRM vs. ChatGPT-Branche (OK/X).", "Begründung bei abweichender Branchenzuordnung.", "FSM-Relevanz: Bewertung, ob das Unternehmen für FSM geeignet ist (OK/X).", "Begründung zur FSM-Bewertung.", "Schätzung Anzahl Mitarbeiter via ChatGPT (nur falls Wiki-Daten fehlen).", "Vergleich CRM vs. Wiki vs. ChatGPT Mitarbeiterzahl (OK/X).", "Begründung bei Mitarbeiterabweichung (Prozentdifferenz).", "Schätzung Servicetechniker via ChatGPT (in Kategorien, z.B. <50, >100, >200, >500).", "Begründung bei Abweichung der Technikerzahl.", "Schätzung Umsatz via ChatGPT.", "Begründung bei Umsatzabweichung.", "Anzahl Kontakte (Serviceleiter) gefunden.", "Anzahl Kontakte (IT-Leiter) gefunden.", "Anzahl Kontakte (Management) gefunden.", "Anzahl Kontakte (Disponent) gefunden.", "Timestamp der Kontaktsuche.", "Timestamp der Wikipedia-Suche.", "Timestamp der ChatGPT-Bewertung.", "Ausgabe der Skriptversion, die das Ergebnis erzeugt hat.", "Token-Zählung (separat pro Modul).", "Roh extrahierter Text der Firmenwebsite (maximal 1000 Zeichen).", "Zusammenfassung des Webseiteninhalts, fokussiert auf Tätigkeitsfeld, Produkte & Leistungen."], - ["Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Wird durch Wikipedia Scraper bereitgestellt", "Wird zunächst nicht verwendet, kann aber zum Vergleich mit der CRM-Beschreibung genutzt werden.", "Wird u.a. zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM-Branche bzw. CRM-Beschreibung Branche Extern verglichen. Stimmen alle drei Einstufungen grob überein, bestärkt dies die ursprüngliche Einstufung. Laufen diese Branchen weit auseinander, soll – sofern der Wikipedia-Artikel verifiziert ist – die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen CRM-Beschreibung Branche Extern und CRM-Branche an dritter Stelle.", "Wird u.a. mit CRM-Umsatz zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wird u.a. mit CRM-Anzahl Mitarbeiter zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wenn Website-Daten fehlen, wird in diesem Feld keine zusätzliche Information einbezogen; ansonsten als zusätzlicher Kontext.", "\"Es soll durch ChatGPT geprüft werden, ob anhand der vorliegenden Daten bestätigt werden kann, dass der Wikipedia-Eintrag das Unternehmen sicher beschreibt. Dabei können alle Daten (Website, Umsatz, Mitarbeiterzahl etc.) berücksichtigt werden. Eine gewisse Toleranz (±30%) ist erlaubt. Insbesondere bei Konzernstrukturen muss großzügig bewertet werden. Abweichungen sollen in der Spalte 'Chat Begründung Wiki Inkonsistenz' begründet werden.\"", "\"Liegt eine Inkonsistenz zwischen dem gefundenen Wikipedia-Artikel und dem Unternehmen vor, so soll dies kurz begründet werden. Wurde der Artikel als unpassend identifiziert, soll ChatGPT einen alternativen Wikipedia-Artikel vorschlagen und diesen in 'Chat Vorschlag Wiki Artikel' ausgeben.\"", "\"Sollte durch die Wikipedia-Suche kein Artikel gefunden werden oder als unpassend bewertet werden, soll ChatGPT eigenständig nach einem passenden Artikel recherchieren. Der gefundene Artikel muss vom als unpassend bewerteten Artikel abweichen. Wird kein passender Artikel gefunden, soll 'kein Artikel verfügbar' ausgegeben werden.\"", "XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx", "\"ChatGPT soll anhand der vorliegenden Informationen prüfen, welcher Branche des Ziel-Branchenschemas das Unternehmen am ehesten zugeordnet werden kann. Das Ziel-Branchenschema darf nicht verändert werden, sondern die Vorschläge müssen exakt diesem Schema entsprechen.\"", "Die in Spalte CRM festgelegte Branche soll mit der von ChatGPT ermittelten Branche in 'Chat Vorschlag Branche' verglichen werden.", "Weicht die von ChatGPT ermittelte Branche von der in CRM vorliegenden ab, so soll ChatGPT die Abweichung kurz begründen.", "ChatGPT soll anhand der vorliegenden Daten prüfen, ob das Unternehmen für den Einsatz einer Field Service Management Lösung geeignet ist.", "Die in 'Chat Begründung für FSM Relevanz' angegebene Begründung soll zur Bewertung der FSM-Eignung herangezogen werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT basierend auf öffentlich verfügbaren Informationen die Mitarbeiterzahl schätzen. Falls keine Schätzung möglich ist, wird 'keine Schätzung möglich' ausgegeben.", "Entspricht die durch ChatGPT ermittelte Mitarbeiterzahl ungefähr den in CRM und Wikipedia ermittelten Werten (±30%), wird 'OK' ausgegeben, andernfalls 'X' und eine Begründung in 'Chat Begründung Abweichung Mitarbeiterzahl'.", "Weicht die von ChatGPT geschätzte Mitarbeiterzahl signifikant von den CRM- oder Wikipedia-Werten ab, soll dies kurz begründet werden.", "ChatGPT soll auf Basis öffentlich zugänglicher Informationen eine Schätzung der Anzahl Servicetechniker abgeben (Kategorisierung: 0, <50, >100, >200, >500). Bei Abweichungen der Recherche-Werte soll 'X' ausgegeben werden, ansonsten 'OK'.", "Weicht die von ChatGPT geschätzte Technikerzahl von den CRM-Werten ab, soll dies begründet werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT den Umsatz anhand der Unternehmenswebsite oder anderer Daten schätzen. Bei fehlender Schätzung soll 'keine Schätzung möglich' ausgegeben werden.", "ChatGPT soll signifikante Umsatzabweichungen zwischen den Schätzungen von Chat, Wikipedia und CRM begründen. Stimmen die Werte (±30%) überein, wird 'OK' ausgegeben.", "Über SerpAPI wird zusammen mit der in 'CRM Kurzform' enthaltenen Information nach 'Serviceleiter' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Leiter IT' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Geschäftsführer' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' erneut nach 'Serviceleiter' gesucht.", "Wenn die Kontaktsuche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die Wikipedia-Suche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die ChatGPT-Bewertung gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wird durch das System befüllt", "Wird durch tiktoken berechnet"] + [ # Spaltenname (Zeile 1) + "ReEval Flag", "CRM Name", "CRM Kurzform", "CRM Website", "CRM Ort", "CRM Beschreibung", "CRM Branche", "CRM Beschreibung Branche extern", "CRM Anzahl Techniker", "CRM Umsatz", "CRM Anzahl Mitarbeiter", "CRM Vorschlag Wiki URL", "Wiki URL", "Wiki Absatz", "Wiki Branche", "Wiki Umsatz", "Wiki Mitarbeiter", "Wiki Kategorien", "Chat Wiki Konsistenzprüfung", "Chat Begründung Wiki Inkonsistenz", "Chat Vorschlag Wiki Artikel", "Begründung bei Abweichung", "Chat Vorschlag Branche", "Chat Konsistenz Branche", "Chat Begründung 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", "Contact Search Timestamp", "Wikipedia Timestamp", "Timestamp letzte Prüfung", "Version", "Tokens", "Website Rohtext", "Website Zusammenfassung", + "Website Scrape Timestamp", # AT (NEU) + "Geschätzter Techniker Bucket", # AU (NEU) + "Finaler Umsatz (Wiki>CRM)", # AV (NEU) + "Finaler Mitarbeiter (Wiki>CRM)" # AW (NEU) + ], + [ # Quelle der Daten (Zeile 2) + "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "System", "System", "System", "System", "System", "Web Scraper", "Chat GPT API", + "System", # AT (NEU) - Timestamp vom Scraping-Prozess + "ML Modell / Skript", # AU (NEU) - Ergebnis der Schätzung + "Skript (Wiki/CRM)", # AV (NEU) - Berechnet nach Priorität + "Skript (Wiki/CRM)" # AW (NEU) - Berechnet nach Priorität + ], + [ # Feldkategorie (Zeile 3) + "Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Beschreibung (Text)", "Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter", "Wikipedia Artikel URL", "Wikipedia Artikel", "Beschreibung (Text)", "Branche", "Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Verifizierung", "Begründung bei Abweichung", "Wikipedia Artikel", "Wikipedia Artikel", "Branche", "Branche", "Branche", "FSM Relevanz", "FSM Relevanz", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Servicetechniker", "Anzahl Servicetechniker", "Umsatz", "Umsatz", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Timestamp", "Timestamp", "Timestamp", "Version des Skripts die verwendet wurde", "ChatGPT Tokens", "Website-Content", "Website Zusammenfassung", + "Timestamp", # AT (NEU) + "Anzahl Servicetechniker Bucket", # AU (NEU) + "Umsatz", # AV (NEU) + "Anzahl Mitarbeiter" # AW (NEU) + ], + [ # Kurze Beschreibung (Zeile 4) + "Systemspalte, irrelevant für den Prompt. Wird zur manuellen Neuprüfung genutzt.", "Enthält den Firmennamen; Normalisierung erfolgt bei der Suche.", "Manuell gepflegte Kurzform, meist die ersten 2 Worte.", "Website des Unternehmens.", "Ort des Unternehmens.", "Kurze Beschreibung des Unternehmens.", "Aktuelle Branchenzuweisung gemäß Ziel-Branchenschema.", "Externe Branchenbeschreibung (z.B. von Dealfront).", "Recherchierte Anzahl Servicetechniker.", "Umsatz in Mio. € (CRM).", "Anzahl Mitarbeiter (CRM).", "Vorgeschlagene Wikipedia URL (Ausgangspunkt).", "Wikipedia URL (Ergebnis der Suche).", "Erster Absatz des Wikipedia-Artikels.", "Wikipedia-Branche – für den Branchenabgleich.", "Wikipedia-Umsatz – zur Validierung.", "Wikipedia-Mitarbeiterzahl – zur Validierung.", "Liste der Wikipedia-Kategorien.", "\"OK\" oder \"X\" – Ergebnis der Wikipedia-Validierung.", "Begründung bei Inkonsistenz (Wiki).", "Chat-Vorschlag Wiki Artikel: Falls kein passender Artikel gefunden, alternativ vorschlagen.", "Nicht genutzt, evtl. für zukünftige Funktionen.", "Branchenvorschlag via ChatGPT (alternativer Vorschlag).", "Vergleich: Übereinstimmung CRM vs. ChatGPT-Branche (OK/X).", "Begründung bei abweichender Branchenzuordnung.", "FSM-Relevanz: Bewertung, ob das Unternehmen für FSM geeignet ist (OK/X).", "Begründung zur FSM-Bewertung.", "Schätzung Anzahl Mitarbeiter via ChatGPT (nur falls Wiki-Daten fehlen).", "Vergleich CRM vs. Wiki vs. ChatGPT Mitarbeiterzahl (OK/X).", "Begründung bei Mitarbeiterabweichung (Prozentdifferenz).", "Schätzung Servicetechniker via ChatGPT (in Kategorien, z.B. <50, >100, >200, >500).", "Begründung bei Abweichung der Technikerzahl.", "Schätzung Umsatz via ChatGPT.", "Begründung bei Umsatzabweichung.", "Anzahl Kontakte (Serviceleiter) gefunden.", "Anzahl Kontakte (IT-Leiter) gefunden.", "Anzahl Kontakte (Management) gefunden.", "Anzahl Kontakte (Disponent) gefunden.", "Timestamp der Kontaktsuche.", "Timestamp der Wikipedia-Suche.", "Timestamp der ChatGPT-Bewertung / Letzte Prüfung der Zeile.", "Ausgabe der Skriptversion, die das Ergebnis erzeugt hat.", "Token-Zählung (separat pro Modul).", "Roh extrahierter Text der Firmenwebsite (maximal 1000 Zeichen).", "Zusammenfassung des Webseiteninhalts, fokussiert auf Tätigkeitsfeld, Produkte & Leistungen.", + "Timestamp des letzten Website-Scrapings (AR, AS).", # AT (NEU) + "Geschätzter Bucket (1-7) für Servicetechniker basierend auf ML-Modell.",# AU (NEU) + "Konsolidierter Umsatz (Mio €) nach Priorität Wiki > CRM.", # AV (NEU) + "Konsolidierte Mitarbeiterzahl nach Priorität Wiki > CRM." # AW (NEU) + ], + [ # Aufgabe / Funktion (Zeile 5) - Hier müssen wir neue Einträge hinzufügen + "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Datenquelle", "Wird durch Wikipedia Scraper bereitgestellt", "Wird zunächst nicht verwendet, kann aber zum Vergleich mit der CRM-Beschreibung genutzt werden.", "Wird u.a. zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM-Branche bzw. CRM-Beschreibung Branche Extern verglichen. Stimmen alle drei Einstufungen grob überein, bestärkt dies die ursprüngliche Einstufung. Laufen diese Branchen weit auseinander, soll – sofern der Wikipedia-Artikel verifiziert ist – die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen CRM-Beschreibung Branche Extern und CRM-Branche an dritter Stelle.", "Wird u.a. mit CRM-Umsatz zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wird u.a. mit CRM-Anzahl Mitarbeiter zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.", "Wenn Website-Daten fehlen, wird in diesem Feld keine zusätzliche Information einbezogen; ansonsten als zusätzlicher Kontext.", "\"Es soll durch ChatGPT geprüft werden, ob anhand der vorliegenden Daten bestätigt werden kann, dass der Wikipedia-Eintrag das Unternehmen sicher beschreibt. Dabei können alle Daten (Website, Umsatz, Mitarbeiterzahl etc.) berücksichtigt werden. Eine gewisse Toleranz (±30%) ist erlaubt. Insbesondere bei Konzernstrukturen muss großzügig bewertet werden. Abweichungen sollen in der Spalte 'Chat Begründung Wiki Inkonsistenz' begründet werden.\"", "\"Liegt eine Inkonsistenz zwischen dem gefundenen Wikipedia-Artikel und dem Unternehmen vor, so soll dies kurz begründet werden. Wurde der Artikel als unpassend identifiziert, soll ChatGPT einen alternativen Wikipedia-Artikel vorschlagen und diesen in 'Chat Vorschlag Wiki Artikel' ausgeben.\"", "\"Sollte durch die Wikipedia-Suche kein Artikel gefunden werden oder als unpassend bewertet werden, soll ChatGPT eigenständig nach einem passenden Artikel recherchieren. Der gefundene Artikel muss vom als unpassend bewerteten Artikel abweichen. Wird kein passender Artikel gefunden, soll 'kein Artikel verfügbar' ausgegeben werden.\"", "XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx", "\"ChatGPT soll anhand der vorliegenden Informationen prüfen, welcher Branche des Ziel-Branchenschemas das Unternehmen am ehesten zugeordnet werden kann. Das Ziel-Branchenschema darf nicht verändert werden, sondern die Vorschläge müssen exakt diesem Schema entsprechen.\"", "Die in Spalte CRM festgelegte Branche soll mit der von ChatGPT ermittelten Branche in 'Chat Vorschlag Branche' verglichen werden.", "Weicht die von ChatGPT ermittelte Branche von der in CRM vorliegenden ab, so soll ChatGPT die Abweichung kurz begründen.", "ChatGPT soll anhand der vorliegenden Daten prüfen, ob das Unternehmen für den Einsatz einer Field Service Management Lösung geeignet ist.", "Die in 'Chat Begründung für FSM Relevanz' angegebene Begründung soll zur Bewertung der FSM-Eignung herangezogen werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT basierend auf öffentlich verfügbaren Informationen die Mitarbeiterzahl schätzen. Falls keine Schätzung möglich ist, wird 'keine Schätzung möglich' ausgegeben.", "Entspricht die durch ChatGPT ermittelte Mitarbeiterzahl ungefähr den in CRM und Wikipedia ermittelten Werten (±30%), wird 'OK' ausgegeben, andernfalls 'X' und eine Begründung in 'Chat Begründung Abweichung Mitarbeiterzahl'.", "Weicht die von ChatGPT geschätzte Mitarbeiterzahl signifikant von den CRM- oder Wikipedia-Werten ab, soll dies kurz begründet werden.", "ChatGPT soll auf Basis öffentlich zugänglicher Informationen eine Schätzung der Anzahl Servicetechniker abgeben (Kategorisierung: 0, <50, >100, >200, >500). Bei Abweichungen der Recherche-Werte soll 'X' ausgegeben werden, ansonsten 'OK'.", "Weicht die von ChatGPT geschätzte Technikerzahl von den CRM-Werten ab, soll dies begründet werden.", "Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT den Umsatz anhand der Unternehmenswebsite oder anderer Daten schätzen. Bei fehlender Schätzung soll 'keine Schätzung möglich' ausgegeben werden.", "ChatGPT soll signifikante Umsatzabweichungen zwischen den Schätzungen von Chat, Wikipedia und CRM begründen. Stimmen die Werte (±30%) überein, wird 'OK' ausgegeben.", "Über SerpAPI wird zusammen mit der in 'CRM Kurzform' enthaltenen Information nach 'Serviceleiter' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Leiter IT' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Geschäftsführer' gesucht.", "Über SerpAPI wird zusammen mit 'CRM Kurzform' erneut nach 'Serviceleiter' gesucht.", "Wenn die Kontaktsuche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die Wikipedia-Suche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wenn die ChatGPT-Bewertung gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.", "Wird durch das System befüllt", "Wird durch tiktoken berechnet", "Wird durch Web Scraper befüllt (z.B. Modus website)", "Wird durch ChatGPT API aus Website Rohtext generiert (z.B. Modus website)", + "Timestamp wird gesetzt, wenn Website Rohtext/Zusammenfassung geschrieben werden.", # AT (NEU) + "Ergebnis der Schätzung durch das trainierte ML-Modell (z.B. Decision Tree).", # AU (NEU) + "Vom Skript berechneter Wert, priorisiert Wiki > CRM. Dient als Input für ML-Modell und zur Transparenz.", # AV (NEU) + "Vom Skript berechneter Wert, priorisiert Wiki > CRM. Dient als Input für ML-Modell und zur Transparenz." # AW (NEU) + ] ] - # Bestimme den Bereich basierend auf der Anzahl der Spalten in der ersten Header-Zeile - num_cols = len(new_headers[0]) - # Konvertiere Spaltenanzahl in Buchstaben (A=1, B=2, ..., Z=26, AA=27, ...) + # Bestimme den Bereich basierend auf der Anzahl der Spalten in der ersten Header-Zeile (jetzt AW) + num_cols = len(new_headers[0]) # Sollte 49 sein (A=1 ... AW=49) def colnum_string(n): string = "" while n > 0: @@ -2442,216 +2766,263 @@ def alignment_demo(sheet): string = chr(65 + remainder) + string return string - end_col_letter = colnum_string(num_cols) - header_range = f"A1:{end_col_letter}{len(new_headers)}" + end_col_letter = colnum_string(num_cols) # Sollte "AW" ergeben + header_range = f"A1:{end_col_letter}{len(new_headers)}" # Sollte A1:AW5 sein try: sheet.update(values=new_headers, range_name=header_range) - print(f"Alignment-Demo abgeschlossen: Header in Bereich {header_range} geschrieben.") # Geändert zu print für Sichtbarkeit + print(f"Alignment-Demo abgeschlossen: Header in Bereich {header_range} geschrieben.") debug_print(f"Alignment-Demo: Header in Bereich {header_range} geschrieben.") except Exception as e: - print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") # Geändert zu print + print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") debug_print(f"FEHLER beim Schreiben der Alignment-Demo Header: {e}") # ==================== DATA PROCESSOR ==================== class DataProcessor: - # Diese Klasse enthält jetzt hauptsächlich die Logik für die Verarbeitung einzelner Zeilen - # und spezifische Modi, die nicht als Batch laufen. + """ + Verarbeitet Daten aus dem Google Sheet, führt verschiedene Anreicherungs- + und Analyseprozesse durch, inklusive Timestamp-basierter Überspringung. + Enthält jetzt auch die Datenvorbereitung für das ML-Modell. + """ def __init__(self, sheet_handler): + """ + Initialisiert den DataProcessor. + + Args: + sheet_handler (GoogleSheetHandler): Eine initialisierte Instanz des GoogleSheetHandlers. + """ self.sheet_handler = sheet_handler self.wiki_scraper = WikipediaScraper() # Eigene Instanz des Scrapers - # @retry_on_failure # Vorsicht mit Retry auf dieser Ebene, kann lange dauern + # @retry_on_failure # Vorsicht mit Retry auf dieser Ebene für die ganze Zeile def _process_single_row(self, row_num_in_sheet, row_data, process_wiki=True, process_chatgpt=True, process_website=True): - """Verarbeitet die Daten für eine einzelne Zeile.""" + """ + Verarbeitet die Daten für eine einzelne Zeile, prüft Timestamps für jeden Teilbereich. + + Args: + row_num_in_sheet (int): Die 1-basierte Zeilennummer im Google Sheet. + row_data (list): Die List der Daten für diese Zeile. + process_wiki (bool): Ob der Wikipedia-Teil ausgeführt werden soll. + process_chatgpt (bool): Ob der ChatGPT-Evaluationsteil ausgeführt werden soll. + process_website (bool): Ob der Website-Teil ausgeführt werden soll. + """ debug_print(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} ---") - - # Verwende COLUMN_MAP für sicherere Zugriffe - # Beispiel: company_name = row_data[COLUMN_MAP["CRM Name"]] if len(row_data) > COLUMN_MAP["CRM Name"] else "" - # Der Einfachheit halber bleiben wir vorerst bei Indizes, aber mit Bewusstsein für die Karte - company_name = row_data[1] if len(row_data) > 1 else "" - website_url = row_data[3] if len(row_data) > 3 else "" - crm_kurzform = row_data[2] if len(row_data) > 2 else company_name # Fallback für Kurzform + updates = [] # Sammle alle Updates für diese Zeile + now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + any_processing_done = False # Flag, ob überhaupt etwas getan wurde + + # --- Daten extrahieren (nutze COLUMN_MAP) --- + # Hilfsfunktion, um sicher auf Index zuzugreifen + def get_cell_value(col_key): + idx = COLUMN_MAP.get(col_key) + if idx is not None and len(row_data) > idx: + return row_data[idx] + return "" # Oder None oder einen anderen Standardwert + + company_name = get_cell_value("CRM Name") + website_url = get_cell_value("CRM Website") + original_website = website_url # Merken für späteren Vergleich + crm_branche = get_cell_value("CRM Branche") + crm_beschreibung = get_cell_value("CRM Beschreibung") + crm_wiki_url = get_cell_value("CRM Vorschlag Wiki URL") + + # Lese aktuelle Werte für Website Rohtext/Zusammenfassung + website_raw = get_cell_value("Website Rohtext") or "k.A." + website_summary = get_cell_value("Website Zusammenfassung") or "k.A." # --- 1. Website Handling (Lookup, Scrape, Summarize) --- - original_website = website_url - website_raw = "k.A." - website_summary = "k.A." - website_details = "k.A." + # Prüfe Timestamp AT (Index 45) + website_ts_needed = process_website and not get_cell_value("Website Scrape Timestamp").strip() - # Website Lookup, wenn leer - if process_website and (not website_url or website_url.strip().lower() == "k.a."): - debug_print(f"Zeile {row_num_in_sheet}: CRM Website fehlt, starte SERP Lookup für '{company_name}'...") - new_website = serp_website_lookup(company_name) - if new_website != "k.A.": - website_url = new_website - debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolgreich: {website_url}") - # Schreibe neue Website direkt zurück (optional, oder sammle für Batch Update) - # self.sheet_handler.sheet.update_cell(row_num_in_sheet, COLUMN_MAP["CRM Website"] + 1, website_url) + if website_ts_needed: + debug_print(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Timestamp AT fehlt)...") + any_processing_done = True + + # Website Lookup, wenn leer + if not website_url or website_url.strip().lower() == "k.a.": + debug_print(f"Zeile {row_num_in_sheet}: CRM Website fehlt, starte SERP Lookup für '{company_name}'...") + new_website = serp_website_lookup(company_name) + if new_website != "k.A.": + website_url = new_website # Aktualisiere URL für weitere Schritte + debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolgreich: {website_url}") + if website_url != original_website: + updates.append({'range': f'D{row_num_in_sheet}', 'values': [[website_url]]}) + else: + debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolglos.") + + # Website Scraping, wenn URL vorhanden + if website_url and website_url.strip().lower() != "k.a.": + debug_print(f"Zeile {row_num_in_sheet}: Starte Website Scraping für {website_url}...") + new_website_raw = get_website_raw(website_url) + new_website_summary = summarize_website_content(new_website_raw) + + # Füge Updates nur hinzu, wenn sich etwas geändert hat oder vorher k.A. war + if new_website_raw != website_raw: + updates.append({'range': f'AR{row_num_in_sheet}', 'values': [[new_website_raw]]}) + website_raw = new_website_raw # Aktualisiere lokalen Wert für Chat-Teil + if new_website_summary != website_summary: + updates.append({'range': f'AS{row_num_in_sheet}', 'values': [[new_website_summary]]}) + website_summary = new_website_summary # Aktualisiere lokalen Wert für Chat-Teil else: - debug_print(f"Zeile {row_num_in_sheet}: SERP Lookup erfolglos.") - - # Website Scraping (Rohtext, Zusammenfassung, Details), wenn URL vorhanden - if process_website and website_url and website_url.strip().lower() != "k.a.": - debug_print(f"Zeile {row_num_in_sheet}: Starte Website Scraping für {website_url}...") - website_raw = get_website_raw(website_url) - website_summary = summarize_website_content(website_raw) # Benötigt OpenAI Key - # Website Details (optional, kann viele Tokens kosten) - # website_details = scrape_website_details(website_url) - # debug_print(f"Zeile {row_num_in_sheet}: Website Details: {website_details[:100]}...") + debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website Scraping (keine gültige URL).") + # Setze Rohtext/Summary auf k.A., falls sie vorher was anderes waren? + if website_raw != "k.A.": updates.append({'range': f'AR{row_num_in_sheet}', 'values': [['k.A.']]}) + if website_summary != "k.A.": updates.append({'range': f'AS{row_num_in_sheet}', 'values': [['k.A.']]}) + website_raw, website_summary = "k.A.", "k.A." # Aktualisiere lokale Werte + + # Setze Website Timestamp (AT) + updates.append({'range': f'AT{row_num_in_sheet}', 'values': [[now_timestamp]]}) + # Version wird am Ende gesetzt + elif process_website: - debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website Scraping (keine gültige URL).") + debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website Verarbeitung (Timestamp AT vorhanden).") + # Stelle sicher, dass lokale Variablen website_raw/summary aktuell sind + website_raw = get_cell_value("Website Rohtext") or "k.A." + website_summary = get_cell_value("Website Zusammenfassung") or "k.A." # --- 2. Wikipedia Handling --- - wiki_data = { # Standardwerte - 'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', - 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.' - } - wiki_timestamp_needed = len(row_data) <= COLUMN_MAP["Wikipedia Timestamp"] or not row_data[COLUMN_MAP["Wikipedia Timestamp"]].strip() - - if process_wiki and wiki_timestamp_needed: - debug_print(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung...") - # Prüfe, ob CRM einen Vorschlag hat - crm_wiki_url = row_data[COLUMN_MAP["CRM Vorschlag Wiki URL"]] if len(row_data) > COLUMN_MAP["CRM Vorschlag Wiki URL"] and row_data[COLUMN_MAP["CRM Vorschlag Wiki URL"]].strip() not in ["", "k.A."] else None - + wiki_data = {} # Wird gefüllt, entweder durch Scraping oder aus Zeile gelesen + wiki_ts_needed = process_wiki and not get_cell_value("Wikipedia Timestamp").strip() + + if wiki_ts_needed: + debug_print(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung (Timestamp AN fehlt)...") + any_processing_done = True + + # Logik für Suche und Extraktion + valid_crm_wiki_url = crm_wiki_url if crm_wiki_url and crm_wiki_url.strip() not in ["", "k.A."] else None article_page = None - if crm_wiki_url: - debug_print(f"Zeile {row_num_in_sheet}: Prüfe CRM Wiki Vorschlag: {crm_wiki_url}") - page = self.wiki_scraper._fetch_page_content(crm_wiki_url.split('/')[-1]) - if page and self.wiki_scraper._validate_article(page, company_name, website_url): + if valid_crm_wiki_url: + debug_print(f"Zeile {row_num_in_sheet}: Prüfe CRM Wiki Vorschlag: {valid_crm_wiki_url}") + page = self.wiki_scraper._fetch_page_content(valid_crm_wiki_url.split('/')[-1]) + # Überprüfe ob website_url hier aktuell ist (könnte durch Lookup geändert sein) + current_website_for_validation = website_url if website_url and website_url != 'k.A.' else original_website + if page and self.wiki_scraper._validate_article(page, company_name, current_website_for_validation): article_page = page else: debug_print(f"Zeile {row_num_in_sheet}: CRM Wiki Vorschlag nicht validiert. Starte Suche...") - article_page = self.wiki_scraper.search_company_article(company_name, website_url) + article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) else: debug_print(f"Zeile {row_num_in_sheet}: Kein CRM Wiki Vorschlag. Starte Suche...") - article_page = self.wiki_scraper.search_company_article(company_name, website_url) + current_website_for_validation = website_url if website_url and website_url != 'k.A.' else original_website + article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) if article_page: debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Daten aus Artikel: {article_page.url}") wiki_data = self.wiki_scraper.extract_company_data(article_page.url) else: debug_print(f"Zeile {row_num_in_sheet}: Kein passender Wikipedia Artikel gefunden.") - wiki_data['url'] = 'Kein Artikel gefunden' # Spezifische Kennung - elif process_wiki: - debug_print(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (Timestamp AN vorhanden).") - # Lade vorhandene Wiki-Daten aus der Zeile, um sie für ChatGPT verfügbar zu machen - wiki_data['url'] = row_data[COLUMN_MAP["Wiki URL"]] if len(row_data) > COLUMN_MAP["Wiki URL"] else 'k.A.' - wiki_data['first_paragraph'] = row_data[COLUMN_MAP["Wiki Absatz"]] if len(row_data) > COLUMN_MAP["Wiki Absatz"] else 'k.A.' - wiki_data['branche'] = row_data[COLUMN_MAP["Wiki Branche"]] if len(row_data) > COLUMN_MAP["Wiki Branche"] else 'k.A.' - wiki_data['umsatz'] = row_data[COLUMN_MAP["Wiki Umsatz"]] if len(row_data) > COLUMN_MAP["Wiki Umsatz"] else 'k.A.' - wiki_data['mitarbeiter'] = row_data[COLUMN_MAP["Wiki Mitarbeiter"]] if len(row_data) > COLUMN_MAP["Wiki Mitarbeiter"] else 'k.A.' - wiki_data['categories'] = row_data[COLUMN_MAP["Wiki Kategorien"]] if len(row_data) > COLUMN_MAP["Wiki Kategorien"] else 'k.A.' + # Setze Standard-k.A.-Werte für wiki_data + wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} - - # --- 3. ChatGPT Evaluationen --- - chat_results = {} # Sammle Ergebnisse der einzelnen ChatGPT Calls - chat_timestamp_needed = len(row_data) <= COLUMN_MAP["Timestamp letzte Prüfung"] or not row_data[COLUMN_MAP["Timestamp letzte Prüfung"]].strip() - - if process_chatgpt and chat_timestamp_needed: - debug_print(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen...") - - # 3.1 Branchenevaluierung (Wichtigste zuerst?) - crm_branche = row_data[COLUMN_MAP["CRM Branche"]] if len(row_data) > COLUMN_MAP["CRM Branche"] else "" - crm_beschreibung = row_data[COLUMN_MAP["CRM Beschreibung"]] if len(row_data) > COLUMN_MAP["CRM Beschreibung"] else "" - chat_results['branche'] = evaluate_branche_chatgpt(crm_branche, crm_beschreibung, wiki_data['branche'], wiki_data['categories'], website_summary) - - # 3.2 Weitere Evaluationen (Beispiele, ggf. anpassen/implementieren) - # chat_results['wiki_verification'] = process_wiki_verification(row_data, wiki_data) # Siehe Batch-Mode - # chat_results['fsm'] = evaluate_fsm_suitability(company_name, wiki_data) - # chat_results['st_estimate'] = evaluate_servicetechnicians_estimate(company_name, wiki_data) - # crm_techniker = row_data[COLUMN_MAP["CRM Anzahl Techniker"]] if len(row_data) > COLUMN_MAP["CRM Anzahl Techniker"] else "k.A." - # internal_category = map_internal_technicians(crm_techniker) - # chat_results['st_explanation'] = evaluate_servicetechnicians_explanation(company_name, chat_results.get('st_estimate'), wiki_data) if internal_category != chat_results.get('st_estimate') else "ok" - # crm_mitarbeiter = row_data[COLUMN_MAP["CRM Anzahl Mitarbeiter"]] if len(row_data) > COLUMN_MAP["CRM Anzahl Mitarbeiter"] else "k.A." - # chat_results['emp_estimate'] = process_employee_estimation(company_name, wiki_data['first_paragraph'], crm_mitarbeiter) - # chat_results['emp_consistency'] = process_employee_consistency(crm_mitarbeiter, wiki_data['mitarbeiter'], chat_results.get('emp_estimate')) - # crm_umsatz = row_data[COLUMN_MAP["CRM Umsatz"]] if len(row_data) > COLUMN_MAP["CRM Umsatz"] else "k.A." - # chat_results['umsatz_estimate'] = evaluate_umsatz_chatgpt(company_name, wiki_data['umsatz']) - # TODO: Vergleich Umsatz / Begründung Abweichung - - # 3.x Token Zählung (Beispielhaft, genauer pro Call) - # total_tokens = token_count(str(wiki_data)) + token_count(str(chat_results)) # Grobe Schätzung - # chat_results['tokens'] = total_tokens - - elif process_chatgpt: - debug_print(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (Timestamp AO vorhanden).") - - # --- 4. Daten für Batch Update sammeln --- - updates = [] - current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # 4.1 Website-Daten - if process_website: - if website_url != original_website: # Nur wenn URL sich geändert hat (durch Lookup) - updates.append({'range': f'D{row_num_in_sheet}', 'values': [[website_url]]}) - updates.append({'range': f'AR{row_num_in_sheet}', 'values': [[website_raw]]}) - updates.append({'range': f'AS{row_num_in_sheet}', 'values': [[website_summary]]}) - # Optional: Details schreiben - # updates.append({'range': f'??{row_num_in_sheet}', 'values': [[website_details]]}) # Braucht neue Spalte - - # 4.2 Wikipedia-Daten (nur wenn neu verarbeitet) - if process_wiki and wiki_timestamp_needed: + # Füge Wiki-Daten zu Updates hinzu updates.append({'range': f'M{row_num_in_sheet}', 'values': [[wiki_data.get('url', 'k.A.')]]}) updates.append({'range': f'N{row_num_in_sheet}', 'values': [[wiki_data.get('first_paragraph', 'k.A.')]]}) updates.append({'range': f'O{row_num_in_sheet}', 'values': [[wiki_data.get('branche', 'k.A.')]]}) updates.append({'range': f'P{row_num_in_sheet}', 'values': [[wiki_data.get('umsatz', 'k.A.')]]}) updates.append({'range': f'Q{row_num_in_sheet}', 'values': [[wiki_data.get('mitarbeiter', 'k.A.')]]}) updates.append({'range': f'R{row_num_in_sheet}', 'values': [[wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'AN{row_num_in_sheet}', 'values': [[current_timestamp]]}) # Wiki Timestamp + # Setze Wiki Timestamp (AN) + updates.append({'range': f'AN{row_num_in_sheet}', 'values': [[now_timestamp]]}) + # Version wird am Ende gesetzt - # 4.3 ChatGPT-Daten (nur wenn neu verarbeitet) - if process_chatgpt and chat_timestamp_needed: - # Branche - updates.append({'range': f'W{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('branch', 'k.A.')]]}) - updates.append({'range': f'X{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('consistency', 'k.A.')]]}) - updates.append({'range': f'Y{row_num_in_sheet}', 'values': [[chat_results.get('branche', {}).get('justification', 'k.A.')]]}) - # Weitere ChatGPT Ergebnisse hier einfügen... - # updates.append({'range': f'Z{row_num_in_sheet}', 'values': [[chat_results.get('fsm', {}).get('suitability', 'k.A.')]]}) - # updates.append({'range': f'AA{row_num_in_sheet}', 'values': [[chat_results.get('fsm', {}).get('justification', 'k.A.')]]}) - # ... etc. ... - # Tokens - # updates.append({'range': f'AQ{row_num_in_sheet}', 'values': [[str(chat_results.get('tokens', 0))]]}) - # Timestamp für ChatGPT/Letzte Prüfung - updates.append({'range': f'AO{row_num_in_sheet}', 'values': [[current_timestamp]]}) - - # 4.4 Immer Update: Version - updates.append({'range': f'AP{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + elif process_wiki: # Wenn nicht benötigt, aber Modus aktiv ist + debug_print(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (Timestamp AN vorhanden).") + # Lade vorhandene Wiki-Daten aus der Zeile, um sie für ChatGPT verfügbar zu machen + wiki_data['url'] = get_cell_value("Wiki URL") or 'k.A.' + wiki_data['first_paragraph'] = get_cell_value("Wiki Absatz") or 'k.A.' + wiki_data['branche'] = get_cell_value("Wiki Branche") or 'k.A.' + wiki_data['umsatz'] = get_cell_value("Wiki Umsatz") or 'k.A.' + wiki_data['mitarbeiter'] = get_cell_value("Wiki Mitarbeiter") or 'k.A.' + wiki_data['categories'] = get_cell_value("Wiki Kategorien") or 'k.A.' + else: # Wenn Modus inaktiv, setze leere Daten + wiki_data = {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} - # --- 5. Batch Update durchführen --- + # --- 3. ChatGPT Evaluationen --- + chat_ts_needed = process_chatgpt and not get_cell_value("Timestamp letzte Prüfung").strip() + + if chat_ts_needed: + debug_print(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Timestamp AO fehlt)...") + any_processing_done = True + + # 3.1 Branchenevaluierung (Nutzt aktuelle wiki_data und website_summary) + branch_result = evaluate_branche_chatgpt( + crm_branche, + crm_beschreibung, + wiki_data.get('branche', 'k.A.'), + wiki_data.get('categories', 'k.A.'), + website_summary # Nutzt den Wert, der ggf. oben aktualisiert wurde + ) + updates.append({'range': f'W{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) + updates.append({'range': f'X{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) + updates.append({'range': f'Y{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) + + # --- HIER weitere ChatGPT-basierte Evaluationen einfügen --- + # Beispiel: FSM-Eignung + # fsm_result = evaluate_fsm_suitability(company_name, wiki_data) + # if fsm_result: + # updates.append({'range': f'Z{row_num_in_sheet}', 'values': [[fsm_result.get('suitability', 'k.A.')]]}) + # updates.append({'range': f'AA{row_num_in_sheet}', 'values': [[fsm_result.get('justification', 'k.A.')]]}) + + # Beispiel: Mitarbeiter-Schätzung etc. + # ... + + # Setze Timestamp letzte Prüfung (AO) + updates.append({'range': f'AO{row_num_in_sheet}', 'values': [[now_timestamp]]}) + # Version wird am Ende gesetzt + + elif process_chatgpt: + debug_print(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (Timestamp AO vorhanden).") + + # --- 4. Abschließende Updates --- + # Setze Version, wenn *irgendetwas* in dieser Zeile verarbeitet wurde + if any_processing_done: + updates.append({'range': f'AP{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + + # --- 5. Batch Update für diese Zeile durchführen --- if updates: - self.sheet_handler.batch_update_cells(updates) - debug_print(f"Zeile {row_num_in_sheet}: Batch-Update erfolgreich.") + # Führe Batch Update über den Handler aus + success = self.sheet_handler.batch_update_cells(updates) + if success: + debug_print(f"Zeile {row_num_in_sheet}: Batch-Update erfolgreich ({len(updates)} Zellen/Bereiche).") + else: + debug_print(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") else: - debug_print(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") + debug_print(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alles übersprungen oder keine Änderungen).") debug_print(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") - # Kurze Pause nach jeder Zeile - time.sleep(max(0.5, Config.RETRY_DELAY / 2)) # Kürzere Pause bei Einzelverarbeitung + # Kurze Pause (optional, aber gut für APIs) + time.sleep(max(0.2, Config.RETRY_DELAY / 10)) def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True): """ Verarbeitet Zeilen sequentiell ab einem Startindex. """ data_rows = self.sheet_handler.get_data() # Daten ohne Header header_rows = 5 - - end_row_index = min(start_row_index + num_rows_to_process, len(data_rows)) if start_row_index >= len(data_rows): debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.") return - - debug_print(f"Verarbeite {end_row_index - start_row_index} Zeilen sequentiell (Index {start_row_index} bis {end_row_index - 1})...") + + # Berechne den Endindex sicher + end_row_index = min(start_row_index + num_rows_to_process, len(data_rows)) + actual_rows_to_process = end_row_index - start_row_index + + if actual_rows_to_process <= 0: + debug_print("Keine Zeilen zur sequenziellen Verarbeitung übrig.") + return + + debug_print(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Index {start_row_index} bis {end_row_index - 1})...") for i in range(start_row_index, end_row_index): + if i >= len(data_rows): # Zusätzliche Sicherheitsprüfung + debug_print(f"WARNUNG: Index {i} überschreitet Datenlänge ({len(data_rows)}). Breche Schleife ab.") + break row_data = data_rows[i] row_num_in_sheet = i + header_rows + 1 # 1-basierter Sheet-Index - - # Überspringe Zeilen vor Zeile 7 generell? Nein, start_row_index sollte das regeln. - # if row_num_in_sheet < 7: continue + # Rufe die detaillierte Verarbeitungsmethode auf self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website) @@ -2661,16 +3032,23 @@ class DataProcessor: data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 + reeval_col_idx = COLUMN_MAP.get("ReEval Flag") + + if reeval_col_idx is None: + debug_print("FEHLER: Spalte 'ReEval Flag' nicht in COLUMN_MAP gefunden. Breche Re-Evaluierung ab.") + return + for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 # Prüfe Flag in Spalte A (Index 0) - if len(row) > COLUMN_MAP["ReEval Flag"] and row[COLUMN_MAP["ReEval Flag"]].strip().lower() == "x": + if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": debug_print(f"Re-Evaluiere Zeile {row_num_in_sheet}...") # Führe volle Verarbeitung für diese Zeile durch self._process_single_row(row_num_in_sheet, row, process_wiki=True, process_chatgpt=True, process_website=True) rows_processed += 1 # Optional: Flag nach Verarbeitung löschen? - # self.sheet_handler.sheet.update_cell(row_num_in_sheet, COLUMN_MAP["ReEval Flag"] + 1, "") + # update_flag = [{'range': f'A{row_num_in_sheet}', 'values': [['']]}] + # self.sheet_handler.batch_update_cells(update_flag) debug_print(f"Re-Evaluierung abgeschlossen. {rows_processed} Zeilen verarbeitet.") @@ -2680,26 +3058,35 @@ class DataProcessor: data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 + reeval_col_idx = COLUMN_MAP.get("ReEval Flag") + website_col_idx = COLUMN_MAP.get("CRM Website") + details_col = f"AR" # Spalte AR für Details? War vorher Rohtext. Ggf. neue Spalte? + + if reeval_col_idx is None or website_col_idx is None: + debug_print("FEHLER: Benötigte Spalten für Modus 23 nicht in COLUMN_MAP gefunden.") + return for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 - # Prüfe Flag in Spalte A (Index 0) - if len(row) > COLUMN_MAP["ReEval Flag"] and row[COLUMN_MAP["ReEval Flag"]].strip().lower() == "x": - website_url = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" + if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": + website_url = row[website_col_idx] if len(row) > website_col_idx else "" if not website_url or website_url.strip().lower() == "k.a.": debug_print(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.") continue - + debug_print(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...") - details = scrape_website_details(website_url) - + details = scrape_website_details(website_url) # Annahme: Diese Funktion existiert + # Speichere das Detail-Ergebnis in Spalte AR (Index 43) - update_data = [{'range': f'AR{row_num_in_sheet}', 'values': [[details]]}] + update_data = [{'range': f'{details_col}{row_num_in_sheet}', 'values': [[details]]}] + # Optional: Timestamp setzen? In AT? + # update_data.append({'range': f'AT{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]}) + self.sheet_handler.batch_update_cells(update_data) - debug_print(f"Zeile {row_num_in_sheet}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte AR geschrieben.") + debug_print(f"Zeile {row_num_in_sheet}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte {details_col} geschrieben.") rows_processed += 1 time.sleep(Config.RETRY_DELAY) - + debug_print(f"Modus 23 abgeschlossen. {rows_processed} Zeilen verarbeitet.") @@ -2709,91 +3096,67 @@ class DataProcessor: data_rows = self.sheet_handler.get_data() header_rows = 5 rows_processed = 0 + website_col_idx = COLUMN_MAP.get("CRM Website") + name_col_idx = COLUMN_MAP.get("CRM Name") + + if website_col_idx is None or name_col_idx is None: + debug_print("FEHLER: Benötigte Spalten für Modus 22 nicht in COLUMN_MAP gefunden.") + return for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 - current_website = row[COLUMN_MAP["CRM Website"]] if len(row) > COLUMN_MAP["CRM Website"] else "" - + current_website = row[website_col_idx] if len(row) > website_col_idx else "" + if not current_website or current_website.strip().lower() == "k.a.": - company_name = row[COLUMN_MAP["CRM Name"]] if len(row) > COLUMN_MAP["CRM Name"] else "" + company_name = row[name_col_idx] if len(row) > name_col_idx else "" if not company_name: debug_print(f"Zeile {row_num_in_sheet}: Übersprungen (kein Firmenname für Lookup).") continue debug_print(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...") - new_website = serp_website_lookup(company_name) + new_website = serp_website_lookup(company_name) # Annahme: Diese Funktion existiert if new_website != "k.A.": update_data = [{'range': f'D{row_num_in_sheet}', 'values': [[new_website]]}] + # Optional: Timestamp setzen? Wo? AT? self.sheet_handler.batch_update_cells(update_data) debug_print(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und in Spalte D eingetragen.") rows_processed += 1 else: debug_print(f"Zeile {row_num_in_sheet}: Keine Website gefunden.") - - time.sleep(Config.RETRY_DELAY) # Pause nach jedem Lookup - + + time.sleep(Config.RETRY_DELAY) + debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.") - def prepare_data_for_modeling(self): # Wird zu einer Methode + # --- NEU: Datenvorbereitung als Methode der Klasse --- + def prepare_data_for_modeling(self): """ Lädt Daten aus dem Google Sheet über den sheet_handler, - bereitet sie für das Decision Tree Modell vor: - - Wählt relevante Spalten aus. - - Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität). - - Filtert nach gültiger Technikerzahl (> 0). - - Erstellt die Zielvariable (Techniker-Bucket). - - Bereitet Features auf (One-Hot Encoding für Branche). - - Behält NaNs in numerischen Features für spätere Imputation. - - Returns: - pandas.DataFrame: Vorbereiteter DataFrame für Training/Test-Split, - oder None bei Fehlern. + bereitet sie für das Decision Tree Modell vor. (Implementierung siehe vorherige Antwort) """ debug_print("Starte Datenvorbereitung für Modellierung...") try: # --- 1. Daten laden & Spalten auswählen --- - # Zugriff auf sheet_values über self.sheet_handler if not self.sheet_handler or not self.sheet_handler.sheet_values: debug_print("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen.") return None - all_data = self.sheet_handler.sheet_values # Verwende die bereits geladenen Daten + all_data = self.sheet_handler.sheet_values if len(all_data) <= 5: debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.") return None - # Annahme: Die ersten Header (Zeile 1) enthalten die Spaltennamen headers = all_data[0] - data_rows = all_data[5:] # Daten ohne die ersten 5 Header-Zeilen + data_rows = all_data[5:] df = pd.DataFrame(data_rows, columns=headers) debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") - # Wähle benötigte Spalten aus ( passe die Schlüssel an deine COLUMN_MAP an!) - # Verwende die global definierte COLUMN_MAP - required_cols_keys_in_map = [ - "CRM Name", - "CRM Branche", - "CRM Umsatz", - "Wiki Umsatz", - "CRM Anzahl Mitarbeiter", - "Wiki Mitarbeiter", - "CRM Anzahl Techniker" # <-- ANPASSEN, falls die bekannte Zahl woanders steht! - ] - - # Finde die tatsächlichen Spaltennamen - cols_to_select = [] - missing_keys = [] - # Wir gehen davon aus, dass die *erste* Zeile (Index 0) die tatsächlichen Headernamen sind - actual_headers = {name: idx for idx, name in enumerate(all_data[0])} - - # Überprüfe, ob alle benötigten Spalten via COLUMN_MAP gefunden werden - # und hole die echten Spaltennamen - # Annahme: COLUMN_MAP mappt Beschreibung (Zeile 4?) zu Index (0-basiert) + # Finde die tatsächlichen Spaltennamen anhand der COLUMN_MAP col_indices = {} + tech_col_key = "CRM Anzahl Techniker" # <- ANPASSEN, FALLS NÖTIG try: - tech_col_key = "CRM Anzahl Techniker" # Schlüssel in COLUMN_MAP anpassen! col_indices = { "name": all_data[0][COLUMN_MAP["CRM Name"]], "branche": all_data[0][COLUMN_MAP["CRM Branche"]], @@ -2811,73 +3174,56 @@ class DataProcessor: debug_print(f"FEHLER: Spaltenindex aus COLUMN_MAP ist außerhalb der Grenzen der Header-Zeile: {e}") return None - df_subset = df[cols_to_select].copy() - # Spalten umbenennen für einfachere Handhabung intern rename_map = {v: k for k, v in col_indices.items()} df_subset.rename(columns=rename_map, inplace=True) - debug_print(f"Benötigte Spalten ausgewählt und umbenannt: {list(df_subset.columns)}") - - # --- 2. Features konsolidieren (Umsatz, Mitarbeiter) --- - # Hilfsfunktion bleibt dieselbe + # --- 2. Features konsolidieren --- def get_valid_numeric(value_str): - if value_str is None or pd.isna(value_str) or value_str == '': return np.nan # Leere Strings auch als NaN + # (Implementierung wie in vorheriger Antwort) + if value_str is None or pd.isna(value_str) or value_str == '': return np.nan try: - val = float(str(value_str).replace(',', '.')) # Komma als Dezimaltrenner erlauben + val = float(str(value_str).replace(',', '.')) return val if val > 0 else np.nan except (ValueError, TypeError): - cleaned_str = re.sub(r'[^\d.]', '', str(value_str)) # Nur Ziffern und Punkt behalten + cleaned_str = re.sub(r'[^\d.]', '', str(value_str)) if not cleaned_str: return np.nan try: val = float(cleaned_str) return val if val > 0 else np.nan - except ValueError: - return np.nan + except ValueError: return np.nan - # Konvertiere Quellen-Spalten und wende Priorisierung an cols_to_process = { 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'), 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter') } - for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): - debug_print(f"Verarbeite '{base_name}' (Wiki: {wiki_col}, CRM: {crm_col})...") - # Stelle sicher, dass Spalten existieren bevor apply angewendet wird + debug_print(f"Verarbeite '{base_name}'...") if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan - wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric) crm_numeric = df_subset[crm_col].apply(get_valid_numeric) - df_subset[final_col] = np.where( - wiki_numeric.notna(), wiki_numeric, # Wiki hat Prio 1 (wenn nicht NaN) - np.where(crm_numeric.notna(), crm_numeric, np.nan) # Sonst CRM (wenn nicht NaN) + wiki_numeric.notna(), wiki_numeric, + np.where(crm_numeric.notna(), crm_numeric, np.nan) ) debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.") - # --- 3. Zielvariable vorbereiten (Technikerzahl) --- - techniker_col = "techniker" # Umbenannter Spaltenname + # --- 3. Zielvariable vorbereiten --- + techniker_col = "techniker" debug_print(f"Verarbeite Zielvariable '{techniker_col}'...") - df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce') - initial_rows = len(df_subset) df_filtered = df_subset[ df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) ].copy() filtered_rows = len(df_filtered) - debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt aufgrund fehlender/ungültiger Technikerzahl.") + debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).") debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}") - - if filtered_rows < 50: # Mindestanzahl für sinnvolles Training - debug_print(f"WARNUNG: Nur {filtered_rows} Zeilen mit gültiger Technikerzahl. Modelltraining möglicherweise nicht sinnvoll.") - # return None # Optional hier abbrechen if filtered_rows == 0: return None - # --- 4. Techniker-Buckets erstellen --- bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)'] @@ -2886,39 +3232,25 @@ class DataProcessor: bins=bins, labels=labels, right=True ) debug_print("Techniker-Buckets erstellt.") - debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") # Relative Häufigkeit - + debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") # --- 5. Kategoriale Features vorbereiten (Branche) --- - branche_col = "branche" # Umbenannter Spaltenname + branche_col = "branche" debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...") - - df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() # Leerzeichen entfernen - - # One-Hot Encoding + df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False) - debug_print(f"One-Hot Encoding für Branche durchgeführt. Neue Spaltenanzahl: {len(df_encoded.columns)}") + debug_print(f"One-Hot Encoding für Branche durchgeführt.") - - # --- 6. Finale Auswahl der Features für das Modell --- + # --- 6. Finale Auswahl --- feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')] feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) - target_column = 'Techniker_Bucket' - original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] # Behalte Original-Technikerzahl und Name für Referenz - + original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy() - for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']: df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') - df_model_ready = df_model_ready.reset_index(drop=True) - debug_print("Datenvorbereitung abgeschlossen.") - debug_print(f"Finaler DataFrame für Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") - # debug_print(f"Feature-Spalten: {feature_columns}") # Kann sehr lang sein - debug_print(f"Ziel-Spalte: {target_column}") - nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() debug_print(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")