diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 8eb37975..28b5bb43 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -3861,8 +3861,9 @@ def alignment_demo(sheet): class DataProcessor: """ 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. + und Analyseprozesse durch, inklusive Timestamp-basierter Überspringung + und erzwungener Neuverarbeitung im Re-Eval-Modus. + Enthält auch die Datenvorbereitung für das ML-Modell. """ def __init__(self, sheet_handler): """ @@ -3872,236 +3873,270 @@ class DataProcessor: sheet_handler (GoogleSheetHandler): Eine initialisierte Instanz des GoogleSheetHandlers. """ self.sheet_handler = sheet_handler - self.wiki_scraper = WikipediaScraper() # Eigene Instanz des Scrapers + # Erstelle eine Instanz des Scrapers für diesen Prozessor + # Annahme: WikipediaScraper ist importiert + self.wiki_scraper = WikipediaScraper() + logging.info("DataProcessor initialisiert.") - # @retry_on_failure # Vorsicht mit Retry auf dieser Ebene für die ganze Zeile + # Die zentrale Methode zur Verarbeitung einer einzelnen Zeile # @retry_on_failure # Retry auf der gesamten Zeile ist riskant - # @retry_on_failure # Retry auf der gesamten Zeile ist riskant -def _process_single_row(self, row_num_in_sheet, row_data, - process_wiki=True, process_chatgpt=True, process_website=True, - force_reeval=False): # <-- Parameter ist entscheidend - """ - Verarbeitet die Daten für eine einzelne Zeile. - Priorisiert Wiki-Artikelsuche/-Validierung VOR Extraktion. - Prüft Timestamps, es sei denn force_reeval=True. - """ - logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---") - updates = [] - now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - any_processing_done = False - wiki_data_updated_in_this_run = False + def _process_single_row(self, row_num_in_sheet, row_data, + process_wiki=True, process_chatgpt=True, process_website=True, + force_reeval=False): # <-- Neuer Parameter + """ + Verarbeitet die Daten für eine einzelne Zeile. + Priorisiert Wiki-Artikelsuche/-Validierung VOR Extraktion. + Prüft Timestamps, es sei denn force_reeval=True. + """ + logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---") + updates = [] + now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + any_processing_done = False + wiki_data_updated_in_this_run = False - # Hilfsfunktion für sicheren Zellenzugriff - def get_cell_value(key): - idx = COLUMN_MAP.get(key) - if idx is not None and len(row_data) > idx: return row_data[idx] - return "" + # Hilfsfunktion für sicheren Zellenzugriff + def get_cell_value(key): + # Annahme: COLUMN_MAP ist global verfügbar + idx = COLUMN_MAP.get(key) + if idx is not None and len(row_data) > idx: + return row_data[idx] + return "" - # Lese initiale Werte - company_name = get_cell_value("CRM Name") - website_url = get_cell_value("CRM Website"); original_website = website_url - crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung") - konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung") - website_raw = get_cell_value("Website Rohtext") or "k.A." - website_summary = get_cell_value("Website Zusammenfassung") or "k.A." + # Lese initiale Werte + company_name = get_cell_value("CRM Name") + website_url = get_cell_value("CRM Website"); original_website = website_url + crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung") + konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung") + website_raw = get_cell_value("Website Rohtext") or "k.A." + website_summary = get_cell_value("Website Zusammenfassung") or "k.A." - final_wiki_data = { - 'url': get_cell_value("Wiki URL") or 'k.A.', 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', - 'branche': get_cell_value("Wiki Branche") or 'k.A.', 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', - 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', 'categories': get_cell_value("Wiki Kategorien") or 'k.A.' - } - final_page_object = None + final_wiki_data = { + 'url': get_cell_value("Wiki URL") or 'k.A.', + 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', + 'branche': get_cell_value("Wiki Branche") or 'k.A.', + 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', + 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', + 'categories': get_cell_value("Wiki Kategorien") or 'k.A.' + } + final_page_object = None - # --- 1. Website Handling (Prüft AT oder force_reeval) --- - website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip() - website_processing_needed = process_website and (force_reeval or website_ts_missing) + # --- 1. Website Handling (Prüft AT oder force_reeval) --- + website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip() + website_processing_needed = process_website and (force_reeval or website_ts_missing) - if website_processing_needed: - any_processing_done = True - logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Grund: {'Re-Eval' if force_reeval else 'AT fehlt'})...") - if not website_url or website_url.strip().lower() == "k.a.": - logging.debug(" -> Suche Website via SERP...") - new_website = serp_website_lookup(company_name) - if new_website != "k.A.": - website_url = new_website - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) - if website_url and website_url.strip().lower() != "k.a.": - logging.debug(f" -> Scrape Rohtext von {website_url}...") - new_website_raw = get_website_raw(website_url) - logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(new_website_raw)})...") - new_website_summary = summarize_website_content(new_website_raw) - website_raw = new_website_raw - website_summary = new_website_summary - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) + if website_processing_needed: + any_processing_done = True + logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Grund: {'Re-Eval' if force_reeval else 'AT fehlt'})...") + if not website_url or website_url.strip().lower() == "k.a.": + logging.debug(" -> Suche Website via SERP...") + # Annahme: serp_website_lookup existiert und nutzt logging + new_website = serp_website_lookup(company_name) + if new_website != "k.A.": + website_url = new_website + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) + if website_url and website_url.strip().lower() != "k.a.": + logging.debug(f" -> Scrape Rohtext von {website_url}...") + # Annahme: get_website_raw existiert und nutzt logging + new_website_raw = get_website_raw(website_url) + logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(str(new_website_raw))})...") # str() für Sicherheit + # Annahme: summarize_website_content existiert und nutzt logging + new_website_summary = summarize_website_content(new_website_raw) + website_raw = new_website_raw + website_summary = new_website_summary + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) + else: + logging.warning(f" -> Keine gültige Website gefunden/vorhanden für {company_name}.") + website_raw, website_summary = "k.A.", "k.A." + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + elif process_website: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).") + + # --- 2. Wikipedia Artikel Findung/Validierung (Prüft AN, S='X(Copied)' oder force_reeval) --- + wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() + status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" + wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse) + url_to_potentially_parse = get_cell_value("Wiki URL").strip() + + if wiki_processing_needed: + any_processing_done = True + logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...") + validated_page = None + # Prüfe zuerst, ob die URL in M direkt valide ist + if url_to_potentially_parse and url_to_potentially_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_potentially_parse.lower().startswith("http"): + logging.debug(f" -> Prüfe Validität der vorhandenen URL aus Spalte M: {url_to_potentially_parse}") + try: + # Verwende die wiki_scraper Instanz der Klasse + page_from_m = wikipedia.page(url_to_potentially_parse.split('/wiki/')[-1].replace('_', ' '), auto_suggest=False, preload=True) + if self.wiki_scraper._validate_article(page_from_m, company_name, website_url): # self. hinzufügen + validated_page = page_from_m + logging.info(f" -> Vorhandene URL aus M '{validated_page.url}' ist valide.") + else: + logging.debug(f" -> Vorhandene URL aus M '{page_from_m.title}' ist NICHT valide.") + except wikipedia.exceptions.PageError: + logging.warning(f" -> Seite für vorhandene URL aus M '{url_to_potentially_parse}' nicht gefunden (PageError).") + except wikipedia.exceptions.DisambiguationError as e_disamb_m: + logging.info(f" -> Vorhandene URL aus M '{url_to_potentially_parse}' ist eine Begriffsklärung. Starte Suche...") + # Verwende die wiki_scraper Instanz der Klasse + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen + except Exception as e_val_m: + logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}") + + # Wenn URL aus M nicht valide war oder keine vorhanden war, starte die Suche + if not validated_page: + logging.info(f" -> Keine valide URL in M gefunden oder Prüfung fehlgeschlagen. Starte Wikipedia-Suche für '{company_name}'...") + # Verwende die wiki_scraper Instanz der Klasse + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen + + # Datenextraktion NACH erfolgreicher Findung/Validierung + if validated_page: + logging.info(f" -> Valider Artikel gefunden/bestätigt: {validated_page.url}. Extrahiere Daten...") + final_page_object = validated_page + # Verwende die wiki_scraper Instanz der Klasse + extracted_data = self.wiki_scraper.extract_company_data(validated_page.url) # self. hinzufügen + final_wiki_data = extracted_data + wiki_data_updated_in_this_run = True + logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.") + else: + logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.") + final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + wiki_data_updated_in_this_run = True + + # Füge Updates für M-R und AN hinzu + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + + # Setze S zurück, wenn nötig + if status_s_indicates_reparse or (url_to_potentially_parse != final_wiki_data.get('url')) or force_reeval: + s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") + if s_idx is not None: + s_let = self.sheet_handler._get_col_letter(s_idx + 1) + updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}) + logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") + + elif process_wiki: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).") + + # --- 3. ChatGPT Evaluationen (Branch etc.) --- + chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() + run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run) + + if run_chat_eval: + logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...") + any_processing_done = True + + # Annahme: evaluate_branche_chatgpt existiert und nutzt logging + branch_result = evaluate_branche_chatgpt( + crm_branche, crm_beschreibung, + final_wiki_data.get('branche', 'k.A.'), + final_wiki_data.get('categories', 'k.A.'), + website_summary + ) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) + + # --- Hier Platz für weitere ChatGPT-Calls --- + + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + + elif process_chatgpt: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).") + + # --- 4. Abschließende Updates --- + if any_processing_done: + # Annahme: Config ist verfügbar + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + + # --- 5. Batch Update für diese Zeile --- + if updates: + logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") + success = self.sheet_handler.batch_update_cells(updates) # Annahme: nutzt logging + if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") else: - logging.warning(f" -> Keine gültige Website gefunden/vorhanden für {company_name}.") - website_raw, website_summary = "k.A.", "k.A." - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - elif process_website: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).") + if not any_processing_done: + logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).") - # --- 2. Wikipedia Artikel Findung/Validierung (Prüft AN, S='X(Copied)' oder force_reeval) --- - wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() - status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" - wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse) - url_to_potentially_parse = get_cell_value("Wiki URL").strip() + logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") + # Annahme: Config ist verfügbar + time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20)) - if wiki_processing_needed: - any_processing_done = True - logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...") - validated_page = None - if url_to_potentially_parse and url_to_potentially_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_potentially_parse.lower().startswith("http"): - logging.debug(f" -> Prüfe Validität der vorhandenen URL aus Spalte M: {url_to_potentially_parse}") - try: - page_from_m = wikipedia.page(url_to_potentially_parse.split('/wiki/')[-1].replace('_', ' '), auto_suggest=False, preload=True) - if self.wiki_scraper._validate_article(page_from_m, company_name, website_url): - validated_page = page_from_m - logging.info(f" -> Vorhandene URL aus M '{validated_page.url}' ist valide.") - else: - logging.debug(f" -> Vorhandene URL aus M '{page_from_m.title}' ist NICHT valide.") - except wikipedia.exceptions.PageError: - logging.warning(f" -> Seite für vorhandene URL aus M '{url_to_potentially_parse}' nicht gefunden (PageError).") - except wikipedia.exceptions.DisambiguationError as e_disamb_m: - logging.info(f" -> Vorhandene URL aus M '{url_to_potentially_parse}' ist eine Begriffsklärung. Starte Suche...") - validated_page = self.wiki_scraper.search_company_article(company_name, website_url) - except Exception as e_val_m: - logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}") - - if not validated_page: - logging.info(f" -> Keine valide URL in M gefunden oder Prüfung fehlgeschlagen. Starte Wikipedia-Suche für '{company_name}'...") - validated_page = self.wiki_scraper.search_company_article(company_name, website_url) - - if validated_page: - logging.info(f" -> Valider Artikel gefunden/bestätigt: {validated_page.url}. Extrahiere Daten...") - final_page_object = validated_page - extracted_data = self.wiki_scraper.extract_company_data(validated_page.url) - final_wiki_data = extracted_data - wiki_data_updated_in_this_run = True - logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.") - else: - logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.") - final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} - wiki_data_updated_in_this_run = True - - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - - if status_s_indicates_reparse or (url_to_potentially_parse != final_wiki_data.get('url')) or force_reeval: - s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") - if s_idx is not None: - s_let = self.sheet_handler._get_col_letter(s_idx + 1) - updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}) - logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") - - elif process_wiki: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).") - - # --- 3. ChatGPT Evaluationen (Branch etc.) --- - chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() - run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run) - - if run_chat_eval: - logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...") - any_processing_done = True - - branch_result = evaluate_branche_chatgpt( - crm_branche, crm_beschreibung, - final_wiki_data.get('branche', 'k.A.'), - final_wiki_data.get('categories', 'k.A.'), - website_summary - ) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) - - # --- Hier Platz für weitere ChatGPT-Calls --- - # z.B. FSM Relevanz, Mitarbeiter-Schätzung etc. - # Denke daran, 'final_wiki_data' und 'website_summary' / 'website_raw' zu verwenden. - - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - - elif process_chatgpt: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).") - - # --- 4. Abschließende Updates --- - if any_processing_done: - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) - - # --- 5. Batch Update für diese Zeile --- - if updates: - logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") - success = self.sheet_handler.batch_update_cells(updates) - if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") - else: - if not any_processing_done: - logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).") - - logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") - time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20)) - - - def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True): + # Methode zur sequenziellen Verarbeitung (ruft _process_single_row ohne force_reeval) + def process_rows_sequentially(self, start_data_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 - - if start_row_index >= len(data_rows): - debug_print("Startindex liegt hinter der letzten Datenzeile. Keine Verarbeitung.") + # Annahme: sheet_handler wurde im __init__ gesetzt + if not self.sheet_handler or not self.sheet_handler.sheet_values: + logging.error("Sheet Handler nicht verfügbar oder keine Daten geladen in process_rows_sequentially.") return - # 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 + data_rows = self.sheet_handler.get_data() # Daten ohne Header + header_rows = 5 # Annahme + + if start_data_index >= len(data_rows): + logging.warning(f"Startindex {start_data_index} liegt hinter der letzten Datenzeile ({len(data_rows)}). Keine Verarbeitung.") + return + + end_row_index = min(start_data_index + num_rows_to_process, len(data_rows)) + actual_rows_to_process = end_row_index - start_data_index if actual_rows_to_process <= 0: - debug_print("Keine Zeilen zur sequenziellen Verarbeitung übrig.") + logging.info("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})...") + logging.info(f"Verarbeite {actual_rows_to_process} Zeilen sequenziell (Daten-Index {start_data_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.") + for i in range(start_data_index, end_row_index): + if i >= len(data_rows): + logging.warning(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 + row_num_in_sheet = i + header_rows + 1 - # Rufe die detaillierte Verarbeitungsmethode auf - self._process_single_row(row_num_in_sheet, row_data, process_wiki, process_chatgpt, process_website) + # Rufe die zentrale Verarbeitungsmethode auf, OHNE force_reeval + try: + self._process_single_row(row_num_in_sheet, row_data, + process_wiki, process_chatgpt, process_website, + force_reeval=False) # HIER ist der Unterschied zu reeval + except Exception as e_seq: + # Fange Fehler bei der Verarbeitung einzelner Zeilen ab, um den Lauf nicht zu stoppen + logging.exception(f"Fehler bei der sequenziellen Verarbeitung von Zeile {row_num_in_sheet}: {e_seq}") + # Optional: Markiere die Zeile im Sheet als fehlerhaft? + + logging.info(f"Sequenzielle Verarbeitung von {actual_rows_to_process} Zeilen abgeschlossen.") + # Methode für den Re-Eval Modus (ruft _process_single_row MIT force_reeval) def process_reevaluation_rows(self, row_limit=None, clear_flag=True): """ Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. - Ruft _process_single_row für jede dieser Zeilen auf. + Ruft _process_single_row für jede dieser Zeilen auf mit force_reeval=True. Verarbeitet maximal row_limit Zeilen. Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung. """ logging.info(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") - # Lade Daten frisch if not self.sheet_handler.load_data(): return all_data = self.sheet_handler.get_all_data_with_headers() if not all_data or len(all_data) <= 5: - logging.warning("Keine Daten für Re-Evaluation gefunden.") - return + logging.warning("Keine Daten für Re-Evaluation gefunden.") + return header_rows = 5 data_rows = all_data[header_rows:] + # Annahme: COLUMN_MAP ist global verfügbar reeval_col_idx = COLUMN_MAP.get("ReEval Flag") if reeval_col_idx is None: - logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") - return + logging.error("FEHLER: 'ReEval Flag' Spaltenindex nicht in COLUMN_MAP gefunden.") + return - # Finde zuerst alle Kandidaten (Zeilennummer im Sheet und Rohdaten) rows_to_process = [] for idx, row in enumerate(data_rows): if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": @@ -4112,107 +4147,67 @@ def _process_single_row(self, row_num_in_sheet, row_data, logging.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") if found_count == 0: - logging.info("Keine Zeilen zur Re-Evaluation markiert.") - return + logging.info("Keine Zeilen zur Re-Evaluation markiert.") + return processed_count = 0 updates_clear_flag = [] - rows_actually_processed = [] # Liste der Zeilennummern, die verarbeitet wurden + rows_actually_processed = [] - # Verarbeite die gefundenen Kandidaten bis zum Limit - # WICHTIG: Iteriere nur über die Kandidatenliste, nicht die gesamten Daten for task in rows_to_process: - # --- KORRIGIERTE LIMIT-PRÜFUNG --- - # Prüfe das Limit *bevor* die Verarbeitung beginnt if row_limit is not None and processed_count >= row_limit: logging.info(f"Zeilenlimit ({row_limit}) für Re-Evaluation erreicht. Breche weitere Verarbeitung ab.") - break # Verlasse die Schleife + break row_num = task['row_num'] row_data = task['data'] - logging.info(f"--- Re-Evaluiere Zeile {row_num} ---") try: - # Führe volle Verarbeitung für diese Zeile durch - # _process_single_row prüft intern Timestamps AN, AT, AO etc. + # Rufe _process_single_row mit force_reeval=True auf self._process_single_row(row_num, row_data, - process_wiki=True, process_chatgpt=True, process_website=True, - force_reeval=True) + process_wiki=True, process_chatgpt=True, process_website=True, + force_reeval=True) # WICHTIG! processed_count += 1 - rows_actually_processed.append(row_num) # Füge zur Liste der verarbeiteten hinzu + rows_actually_processed.append(row_num) - # Optional: Flag nach *erfolgreicher* Verarbeitung löschen (Sammeln) if clear_flag: - flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) - if flag_col_letter: # Nur wenn Buchstabe gültig - updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) + flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) + if flag_col_letter: + updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) except Exception as e_proc: - # Logge Fehler, aber mache mit der nächsten Zeile weiter logging.exception(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") - # Flag hier nicht löschen, damit es beim nächsten Mal versucht wird - # Lösche Flags am Ende gebündelt für die *erfolgreich* verarbeiteten Zeilen if clear_flag and updates_clear_flag: - # Logge, welche Flags gelöscht werden - logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...") - success = self.sheet_handler.batch_update_cells(updates_clear_flag) - if not success: - logging.error("FEHLER beim Löschen der ReEval-Flags.") + logging.info(f"Lösche ReEval-Flags für {len(updates_clear_flag)} erfolgreich verarbeitete Zeilen ({rows_actually_processed})...") + success = self.sheet_handler.batch_update_cells(updates_clear_flag) + if not success: + logging.error("FEHLER beim Löschen der ReEval-Flags.") logging.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit war: {row_limit}, Gefunden: {found_count}).") - def process_website_details_for_marked_rows(self): - """ Neuer Modus 23: Extrahiert Website-Details für markierte Zeilen. """ - debug_print("Starte Modus 23: Website Detail Extraction für Zeilen mit 'x' in Spalte A.") - 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 - 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) # Annahme: Diese Funktion existiert - - # Speichere das Detail-Ergebnis in Spalte AR (Index 43) - 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 {details_col} geschrieben.") - rows_processed += 1 - time.sleep(Config.RETRY_DELAY) - - debug_print(f"Modus 23 abgeschlossen. {rows_processed} Zeilen verarbeitet.") - - + # Methode für SERP API Website Lookup def process_serp_website_lookup_for_empty(self): """ Neuer Modus 22: Füllt fehlende Websites via SERP API. """ - debug_print("Starte Modus 22: SERP API Website Lookup für leere Zellen in Spalte D.") + logging.info("Starte Modus: SERP API Website Lookup für leere Zellen in Spalte D.") + # Annahme: sheet_handler wurde initialisiert + if not self.sheet_handler.load_data(): return 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.") + try: + website_col_idx = COLUMN_MAP["CRM Website"] + name_col_idx = COLUMN_MAP["CRM Name"] + website_col_letter = self.sheet_handler._get_col_letter(website_col_idx + 1) + except KeyError as e: + logging.critical(f"FEHLER: Benötigte Spalte '{e}' für Modus nicht in COLUMN_MAP.") + return + except Exception as e: + logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}") return + updates = [] for i, row in enumerate(data_rows): row_num_in_sheet = i + header_rows + 1 current_website = row[website_col_idx] if len(row) > website_col_idx else "" @@ -4220,158 +4215,220 @@ def _process_single_row(self, row_num_in_sheet, row_data, if not current_website or current_website.strip().lower() == "k.a.": 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).") + logging.warning(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) # Annahme: Diese Funktion existiert + logging.info(f"Zeile {row_num_in_sheet}: Suche Website für '{company_name}'...") + # Annahme: serp_website_lookup existiert und nutzt logging + new_website = serp_website_lookup(company_name) + time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.3) # Kurze Pause + 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.") + updates.append({'range': f'{website_col_letter}{row_num_in_sheet}', 'values': [[new_website]]}) + logging.info(f"Zeile {row_num_in_sheet}: Neue Website '{new_website}' gefunden und zum Update hinzugefügt.") rows_processed += 1 else: - debug_print(f"Zeile {row_num_in_sheet}: Keine Website gefunden.") + logging.info(f"Zeile {row_num_in_sheet}: Keine Website gefunden.") + # Optional: Limit für diesen Modus? + # if row_limit is not None and rows_processed >= row_limit: break - time.sleep(Config.RETRY_DELAY) + if updates: + logging.info(f"Sende Batch-Update für {rows_processed} gefundene Websites...") + self.sheet_handler.batch_update_cells(updates) + else: + logging.info("Keine fehlenden Websites gefunden zum Aktualisieren.") - debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.") + logging.info(f"Modus 'website_lookup' abgeschlossen. {rows_processed} Websites ergänzt.") - # --- NEU: Datenvorbereitung als Methode der Klasse --- + # Methode für experimentelle Website Details + def process_website_details_for_marked_rows(self): + """ Neuer Modus 23 (EXPERIMENTELL): Extrahiert Website-Details für markierte Zeilen. """ + logging.info("Starte Modus (EXPERIMENTELL): Website Detail Extraction für Zeilen mit 'x' in Spalte A.") + if not self.sheet_handler.load_data(): return + data_rows = self.sheet_handler.get_data() + header_rows = 5 + rows_processed = 0 + + try: + reeval_col_idx = COLUMN_MAP["ReEval Flag"] + website_col_idx = COLUMN_MAP["CRM Website"] + details_col_idx = COLUMN_MAP["Website Rohtext"] # Nutze AR für Details? Besser neue Spalte! + details_col_letter = self.sheet_handler._get_col_letter(details_col_idx + 1) + # at_col_letter = self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1) # Für Timestamp + except KeyError as e: + logging.critical(f"FEHLER: Benötigte Spalte '{e}' für Modus nicht in COLUMN_MAP.") + return + except Exception as e: + logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}") + return + + updates = [] + for i, row in enumerate(data_rows): + row_num_in_sheet = i + header_rows + 1 + 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.": + logging.warning(f"Zeile {row_num_in_sheet}: Keine gültige Website in Spalte D vorhanden, überspringe.") + continue + + logging.info(f"Zeile {row_num_in_sheet}: Extrahiere Website Details von {website_url}...") + # Annahme: Funktion scrape_website_details existiert + try: + details = scrape_website_details(website_url) + except NameError: + logging.error("Funktion 'scrape_website_details' ist nicht definiert!") + details = "FEHLER: Funktion nicht definiert" + except Exception as e_detail: + logging.exception(f"Fehler bei scrape_website_details für {website_url}: {e_detail}") + details = f"FEHLER: {e_detail}" + + updates.append({'range': f'{details_col_letter}{row_num_in_sheet}', 'values': [[str(details)]]}) # In AR schreiben + # Optional: Timestamp in AT setzen + # updates.append({'range': f'{at_col_letter}{row_num_in_sheet}', 'values': [[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]]}) + + rows_processed += 1 + time.sleep(getattr(Config, 'RETRY_DELAY', 5) * 0.2) # Kleine Pause + + if updates: + logging.info(f"Sende Batch-Update für {rows_processed} Detail-Extraktionen...") + self.sheet_handler.batch_update_cells(updates) + else: + logging.info("Keine Zeilen mit 'x' gefunden für Detail-Extraktion.") + + logging.info(f"Modus 'website_details' abgeschlossen. {rows_processed} Zeilen verarbeitet.") + + + # Methode zur Datenvorbereitung für ML 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. (Implementierung siehe vorherige Antwort) + bereitet sie für das Decision Tree Modell vor. """ - debug_print("Starte Datenvorbereitung für Modellierung...") + logging.info("Starte Datenvorbereitung für Modellierung...") + # Annahme: sheet_handler ist initialisiert und hat Daten geladen + if not self.sheet_handler or not self.sheet_handler.sheet_values: + logging.error("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen für prepare_data.") + if not self.sheet_handler.load_data(): # Versuch nachzuladen + logging.critical("Konnte Daten auch nach erneutem Versuch nicht laden.") + return None - try: - # --- 1. Daten laden & Spalten auswählen --- - 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 - if len(all_data) <= 5: - debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.") - return None - - headers = all_data[0] - 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.") - - # Finde die tatsächlichen Spaltennamen anhand der COLUMN_MAP - col_indices = {} - tech_col_key = "CRM Anzahl Techniker" # <- ANPASSEN, FALLS NÖTIG - try: - col_indices = { - "name": all_data[0][COLUMN_MAP["CRM Name"]], - "branche": all_data[0][COLUMN_MAP["CRM Branche"]], - "umsatz_crm": all_data[0][COLUMN_MAP["CRM Umsatz"]], - "umsatz_wiki": all_data[0][COLUMN_MAP["Wiki Umsatz"]], - "ma_crm": all_data[0][COLUMN_MAP["CRM Anzahl Mitarbeiter"]], - "ma_wiki": all_data[0][COLUMN_MAP["Wiki Mitarbeiter"]], - "techniker": all_data[0][COLUMN_MAP[tech_col_key]] - } - cols_to_select = list(col_indices.values()) - except KeyError as e: - debug_print(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden oder Spalte nicht im Header.") - return None - except IndexError as e: - 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() - 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 --- - def get_valid_numeric(value_str): - # (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(',', '.')) - return val if val > 0 else np.nan - except (ValueError, TypeError): - 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 - - 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}'...") - 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, - 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 --- - 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 (fehlende/ungültige Technikerzahl).") - debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}") - 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)'] - df_filtered['Techniker_Bucket'] = pd.cut( - df_filtered['Anzahl_Servicetechniker_Numeric'], - 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)}") - - # --- 5. Kategoriale Features vorbereiten (Branche) --- - 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() - 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.") - - # --- 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'] - 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.") - nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() - debug_print(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") - - return df_model_ready - - except Exception as e: - debug_print(f"FEHLER während der Datenvorbereitung: {e}") - import traceback - debug_print(traceback.format_exc()) + all_data = self.sheet_handler.sheet_values # Verwende die Daten aus dem Handler + if len(all_data) <= 5: + logging.error("Fehler: Nicht genügend Datenzeilen im Sheet gefunden für Modellierung.") return None + try: # Fange Fehler beim Zugriff auf Header ab + headers = all_data[0] + except IndexError: + logging.critical("FEHLER: Sheet scheint leer zu sein, keine Header gefunden.") + return None + data_rows = all_data[5:] + + df = pd.DataFrame(data_rows, columns=headers) + logging.info(f"DataFrame für Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") + + # Notwendige Schlüssel aus COLUMN_MAP holen + try: + col_keys = { + "name": "CRM Name", "branche": "CRM Branche", "umsatz_crm": "CRM Umsatz", + "umsatz_wiki": "Wiki Umsatz", "ma_crm": "CRM Anzahl Mitarbeiter", + "ma_wiki": "Wiki Mitarbeiter", "techniker": "CRM Anzahl Techniker" # ANPASSEN WENN NÖTIG + } + col_indices = {key: COLUMN_MAP[val] for key, val in col_keys.items()} + # Erstelle Liste der Spaltennamen basierend auf den Headern im Sheet + cols_to_select = [headers[idx] for idx in col_indices.values()] + # Mapping von echtem Header zu internem Namen + rename_map = {headers[idx]: key for key, idx in col_indices.items()} + except KeyError as e: + logging.critical(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden.") + return None + except IndexError as e: + logging.critical(f"FEHLER: Spaltenindex aus COLUMN_MAP ({e}) außerhalb der Grenzen der Header-Zeile ({len(headers)} Spalten). Prüfe COLUMN_MAP!") + return None + + try: # Fange Fehler beim Auswählen der Spalten ab + df_subset = df[cols_to_select].copy() + df_subset.rename(columns=rename_map, inplace=True) + except KeyError as e: + logging.critical(f"FEHLER beim Auswählen/Umbenennen der Spalten: {e}. Verfügbare Spalten: {list(df.columns)}") + return None + + logging.info(f"Benötigte Spalten für Modellierung ausgewählt und umbenannt: {list(df_subset.columns)}") + + # --- Konsolidierung (wie in vorheriger Antwort, mit Logging) --- + def get_valid_numeric(value_str): + if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': return np.nan + original_value = value_str + try: + cleaned_str = str(value_str).replace('.', '').replace("'", "").replace(',', '.') # Auch Apostroph entfernen + val = float(cleaned_str) + return val if val > 0 else np.nan + except (ValueError, TypeError): + logging.debug(f"Konntze Wert '{original_value}' nicht direkt in Float umwandeln.") + 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: + logging.debug(f"Konntze auch bereinigten String '{cleaned_str}' aus '{original_value}' nicht umwandeln.") + return np.nan + + 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(): + logging.info(f"Verarbeite und konsolidiere '{base_name}' (Priorität: Wiki > CRM)...") + 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, crm_numeric) # Vereinfacht: Wiki > CRM, sonst NaN + logging.info(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.") + + # --- Zielvariable vorbereiten (wie in vorheriger Antwort, mit Logging) --- + techniker_col = "techniker" + logging.info(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) + removed_rows = initial_rows - filtered_rows + if removed_rows > 0: logging.info(f"{removed_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).") + logging.info(f"Verbleibende Zeilen für Modellierung: {filtered_rows}") + if filtered_rows == 0: logging.error("FEHLER: Keine Zeilen mit gültiger Technikerzahl übrig!"); return None + + # --- Techniker-Buckets erstellen (wie gehabt) --- + 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)'] + df_filtered['Techniker_Bucket'] = pd.cut(df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins, labels=labels, right=True) + logging.info("Techniker-Buckets erstellt.") + logging.debug(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") + + # --- Kategoriale Features (Branche) (wie gehabt) --- + branche_col = "branche" + logging.info(f"Verarbeite kategoriales Feature '{branche_col}' für 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) + logging.info(f"One-Hot Encoding für '{branche_col}' durchgeführt.") + + # --- Finale Auswahl (wie gehabt) --- + 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'] # 'name' statt 'CRM Name' intern + 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) + + logging.info("Datenvorbereitung für Modellierung abgeschlossen.") + nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() + logging.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") + + return df_model_ready + # ==================== MAIN FUNCTION ==================== # ==================== MAIN FUNCTION ====================