diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 0aaad8cd..bd4e2611 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -69,7 +69,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Optional # ==================== KONFIGURATION ==================== class Config: # ... (Alle deine bisherigen Config-Einstellungen) ... - VERSION = "v1.6.4" # Versionsnummer erhöhen + VERSION = "v1.6.5" # Versionsnummer erhöhen LANG = "de" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" MAX_RETRIES = 3 @@ -3814,67 +3814,85 @@ class DataProcessor: 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. - Verarbeitet maximal row_limit Zeilen. - Löscht optional das 'x'-Flag nach erfolgreicher Verarbeitung. - """ - debug_print(f"Starte Re-Evaluierungsmodus (Spalte A = 'x'). Max. Zeilen: {row_limit if row_limit is not None else 'Unbegrenzt'}") + """ + Verarbeitet nur Zeilen, die in Spalte A mit 'x' markiert sind. + Ruft _process_single_row für jede dieser Zeilen auf. + 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, um aktuelle Flags zu sehen - 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: return - header_rows = 5 - data_rows = all_data[header_rows:] + # 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 + header_rows = 5 + data_rows = all_data[header_rows:] - reeval_col_idx = COLUMN_MAP.get("ReEval Flag") - if reeval_col_idx is None: return debug_print("FEHLER: 'ReEval Flag' nicht in COLUMN_MAP.") + 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 - rows_to_process = [] - # Finde zuerst alle Kandidaten - for idx, row in enumerate(data_rows): - if len(row) > reeval_col_idx and row[reeval_col_idx].strip().lower() == "x": - row_num_in_sheet = idx + header_rows + 1 - rows_to_process.append({'row_num': row_num_in_sheet, 'data': row}) + # 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": + row_num_in_sheet = idx + header_rows + 1 + rows_to_process.append({'row_num': row_num_in_sheet, 'data': row}) - debug_print(f"{len(rows_to_process)} Zeilen mit ReEval-Flag 'x' gefunden.") + found_count = len(rows_to_process) + logging.info(f"{found_count} Zeilen mit ReEval-Flag 'x' gefunden.") - processed_count = 0 - updates_clear_flag = [] + if found_count == 0: + logging.info("Keine Zeilen zur Re-Evaluation markiert.") + return - # Verarbeite die gefundenen Kandidaten bis zum Limit - for task in rows_to_process: - if row_limit is not None and processed_count >= row_limit: - debug_print(f"Zeilenlimit ({row_limit}) erreicht. Breche Re-Evaluation ab.") - break + processed_count = 0 + updates_clear_flag = [] + rows_actually_processed = [] # Liste der Zeilennummern, die verarbeitet wurden - row_num = task['row_num'] - row_data = task['data'] - debug_print(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 - self._process_single_row(row_num, row_data, process_wiki=True, process_chatgpt=True, process_website=True) - processed_count += 1 + # 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 - # Optional: Flag nach Verarbeitung löschen - if clear_flag: - flag_col_letter = self.sheet_handler._get_col_letter(reeval_col_idx + 1) - updates_clear_flag.append({'range': f'{flag_col_letter}{row_num}', 'values': [['']]}) + 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. + self._process_single_row(row_num, row_data, process_wiki=True, process_chatgpt=True, process_website=True) + processed_count += 1 + rows_actually_processed.append(row_num) # Füge zur Liste der verarbeiteten hinzu - except Exception as e_proc: - debug_print(f"FEHLER bei Re-Evaluation von Zeile {row_num}: {e_proc}") - # Flag hier nicht löschen, damit es beim nächsten Mal versucht wird + # 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': [['']]}) - # Lösche Flags am Ende gebündelt - if clear_flag and updates_clear_flag: - debug_print(f"Lösche ReEval-Flags für {len(updates_clear_flag)} verarbeitete Zeilen...") - success = self.sheet_handler.batch_update_cells(updates_clear_flag) - if not success: debug_print("FEHLER beim Löschen der ReEval-Flags.") + 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 - debug_print(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit: {row_limit}).") + # 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"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Limit war: {row_limit}, Gefunden: {found_count}).") def process_website_details_for_marked_rows(self): @@ -4088,214 +4106,346 @@ class DataProcessor: return None +# ==================== MAIN FUNCTION ==================== # ==================== MAIN FUNCTION ==================== def main(): + # WICHTIG: Global LOG_FILE wird benötigt, aber erst nach Arg-Parsing gesetzt. global LOG_FILE + + # --- Logging Setup (Konfiguration von Level und Format) --- + # Diese Konfiguration wird wirksam, sobald die Handler hinzugefügt werden. import logging - # Ganz am Anfang von main, vor dem ersten debug_print - log_level = logging.DEBUG # Explizit setzen - logging.basicConfig(level=log_level, - format='%(asctime)s - %(levelname)s - %(name)s - %(message)s', # Name des Loggers hinzufügen - filename=LOG_FILE, # In Datei schreiben - filemode='a') # Anfügen - # Optional: Auch auf Konsole ausgeben + log_level = logging.DEBUG # Explizit DEBUG setzen für detaillierte Logs + log_format = '%(asctime)s - %(levelname)-8s - %(name)-15s - %(message)s' # Angepasstes Format + + # Root-Logger konfigurieren (noch ohne File Handler) + logging.basicConfig(level=log_level, format=log_format, handlers=[]) # WICHTIG: handlers=[] verhindert default Console Handler + + # Console Handler explizit hinzufügen console_handler = logging.StreamHandler() - console_handler.setLevel(log_level) - console_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')) - logging.getLogger('').addHandler(console_handler) + console_handler.setLevel(log_level) # Nimm das globale Level + console_handler.setFormatter(logging.Formatter(log_format)) + logging.getLogger('').addHandler(console_handler) # Füge zum Root-Logger hinzu + # --- Ende Initial Logging Setup --- - # Testnachricht nach Logging-Setup - logging.debug("DEBUG Logging ist konfiguriert.") - logging.info("INFO Logging ist konfiguriert.") + # Testnachricht (geht nur an Konsole, da File Handler noch fehlt) + logging.debug("DEBUG Logging initial konfiguriert (nur Konsole).") + logging.info("INFO Logging initial konfiguriert (nur Konsole).") - # --- Initialisierung --- - parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript") - # NEU: 'update_wiki' hinzugefügt + # --- Initialisierung (Argument Parser etc.) --- + parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript v1.6.5") # Version aktualisiert valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model", "update_wiki"] parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})") parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen", default=None) + parser.add_argument("--start_row", type=int, help="Startzeile im Sheet (1-basiert) für sequenzielle Modi", default=None) # Optionaler Startpunkt parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad für Modell (.pkl)") parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Pfad für Imputer (.pkl)") parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)") args = parser.parse_args() - Config.load_api_keys() + # Lade API Keys direkt am Anfang + Config.load_api_keys() # Nutzt jetzt logging.debug intern # Betriebsmodus ermitteln mode = None - if args.mode and args.mode.lower() in valid_modes: mode = args.mode.lower(); print(f"Betriebsmodus (aus Kommandozeile): {mode}") + if args.mode and args.mode.lower() in valid_modes: + mode = args.mode.lower() + logging.info(f"Betriebsmodus (aus Kommandozeile): {mode}") else: # Interaktive Abfrage - print("Bitte wählen Sie den Betriebsmodus:") + print("\nBitte wählen Sie den Betriebsmodus:") + # ... (print-Anweisungen für Modi bleiben gleich) ... print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)") print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)") print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)") print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") - print(" update_wiki: Wiki-URL aus Spalte T übernehmen & Reparse/Re-Branch") # NEU - print(" reeval: Verarbeitet Zeilen mit 'x' (volle Verarbeitung, alle TS prüfen)") + print(" update_wiki: Wiki-URL aus Spalte U nach M übernehmen & ReEval-Flag setzen") + print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)") print(" website_lookup: Sucht fehlende Websites (D)") - print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR)") + print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. anpassen print(" contacts: Sucht LinkedIn Kontakte (AM)") print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)") print(" alignment: Schreibt Header A1:AX5 (!)") print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung") + try: mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower() - if mode_input in valid_modes: mode = mode_input - else: print("Ungültige Eingabe -> combined"); mode = "combined" - except Exception as e: print(f"Fehler Modus-Eingabe ({e}) -> combined"); mode = "combined" + if mode_input in valid_modes: + mode = mode_input + else: + print("Ungültige Eingabe -> Standard: combined") + mode = "combined" + except Exception as e: + print(f"Fehler Modus-Eingabe ({e}) -> Standard: combined") + mode = "combined" + logging.info(f"Betriebsmodus (interaktiv gewählt): {mode}") # Zeilenlimit ermitteln row_limit = None if args.limit is not None: - if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") - else: print("Warnung: Negatives Limit ignoriert."); row_limit = None - elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: # Nur für relevante Modi fragen - try: - limit_input = input("Max Zeilen? (Enter=alle): "); - if limit_input.strip(): - try: - limit_val = int(limit_input) - if limit_val >= 0: row_limit = limit_val; print(f"Zeilenlimit: {row_limit}") - else: print("Negatives Limit -> Kein Limit"); row_limit = None - except ValueError: print("Ungültige Zahl -> Kein Limit"); row_limit = None - else: row_limit = None; print("Kein Zeilenlimit.") - except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None + if args.limit >= 0: + row_limit = args.limit + logging.info(f"Zeilenlimit (aus Kommandozeile): {row_limit}") + else: + logging.warning("Warnung: Negatives Limit ignoriert.") + row_limit = None + elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]: # Relevante Modi + try: + limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ") + if limit_input.strip(): + try: + limit_val = int(limit_input) + if limit_val >= 0: + row_limit = limit_val + logging.info(f"Zeilenlimit (interaktiv): {row_limit}") + else: + logging.warning("Negatives Limit -> Kein Limit") + row_limit = None + except ValueError: + logging.warning("Ungültige Zahl -> Kein Limit") + row_limit = None + else: + logging.info("Kein Zeilenlimit angegeben.") + row_limit = None + except Exception as e: + logging.error(f"Fehler Limit-Eingabe ({e}) -> Kein Limit") + row_limit = None + # --- Logdatei-Konfiguration abschließen --- + LOG_FILE = create_log_filename(mode) # Annahme: Funktion existiert und gibt Pfad zurück + try: + file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8') + file_handler.setLevel(log_level) # Nimm das globale Level + file_handler.setFormatter(logging.Formatter(log_format)) + logging.getLogger('').addHandler(file_handler) # Füge zum Root-Logger hinzu + logging.info(f"Logging wird jetzt auch in Datei geschrieben: {LOG_FILE}") + except Exception as e: + logging.error(f"Konnte FileHandler für Logdatei '{LOG_FILE}' nicht erstellen: {e}") + # Programm kann weiterlaufen, loggt aber nur auf Konsole + # --- Ende Logdatei-Konfiguration --- - # Logfile initialisieren - LOG_FILE = create_log_filename(mode) - debug_print(f"===== Skript gestartet ====="); debug_print(f"Version: {Config.VERSION}") - debug_print(f"Betriebsmodus: {mode}"); + # --- Logge finale Startinfos (jetzt auch in Datei) --- + logging.info(f"===== Skript gestartet =====") + logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein + logging.info(f"Betriebsmodus: {mode}") limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' - if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: - limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' - if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)' - debug_print(f"Zeilenlimit: {limit_log_text}") - debug_print(f"Logdatei: {LOG_FILE}") + if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]: + limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt' + if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)' + logging.info(f"Zeilenlimit: {limit_log_text}") + logging.info(f"Logdatei: {LOG_FILE}") + # --- Ende finale Startinfos --- - # --- Vorbereitung --- - load_target_schema() - try: sheet_handler = GoogleSheetHandler(); - except Exception as e: debug_print(f"FATAL: Init GSheet: {e}"); print(f"FEHLER GSheet. Log: {LOG_FILE}"); return - data_processor = DataProcessor(sheet_handler) + # --- Vorbereitung (Sheet Handler etc.) --- + load_target_schema() # Nutzt jetzt logging intern + try: + sheet_handler = GoogleSheetHandler() # Nutzt jetzt logging intern + except Exception as e: + logging.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}") + logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}") + return # Beende Skript, wenn Sheet nicht geladen werden kann + data_processor = DataProcessor(sheet_handler) # Nutzt jetzt logging intern # --- Modusausführung --- start_time = time.time() - debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...") + logging.info(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...") try: # Batch-Modi über Dispatcher if mode in ["wiki", "website", "branch", "summarize", "combined"]: - if row_limit == 0: debug_print("Limit 0 -> Skip Dispatcher.") - else: run_dispatcher(mode, sheet_handler, row_limit) + if row_limit == 0: + logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.") + else: + run_dispatcher(mode, sheet_handler, row_limit) # Nutzt jetzt logging intern + # Einzelne Zeilen Modi (kein Batch-Dispatcher) - elif mode == "reeval": data_processor.process_reevaluation_rows() - elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty() - elif mode == "website_details": data_processor.process_website_details_for_marked_rows() - elif mode == "contacts": process_contact_research(sheet_handler) + elif mode == "reeval": + data_processor.process_reevaluation_rows(row_limit=row_limit) # Nutzt jetzt logging intern + + elif mode == "website_lookup": + data_processor.process_serp_website_lookup_for_empty() # Nutzt jetzt logging intern + + elif mode == "website_details": + logging.warning("Modus 'website_details' ist experimentell.") + data_processor.process_website_details_for_marked_rows() # Nutzt jetzt logging intern + + elif mode == "contacts": + process_contact_research(sheet_handler) # Nutzt jetzt logging intern + elif mode == "full_run": - if row_limit == 0: debug_print("Limit 0 -> Skip full_run.") - else: - start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") - if start_index != -1 and start_index < len(sheet_handler.get_data()): - num_available = len(sheet_handler.get_data()) - start_index - num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available - if num_to_process > 0: - # Übergebe Flags an process_rows_sequentially - data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True) - else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten.") - else: debug_print(f"Startindex {start_index} für 'full_run' ungültig.") + if row_limit == 0: + logging.info("Limit 0 angegeben -> Überspringe full_run.") + else: + # Prüfe, ob eine explizite Startzeile übergeben wurde + if args.start_row and args.start_row > 5: + start_data_index = args.start_row - 5 - 1 # Konvertiere zu 0-basiertem Datenindex + logging.info(f"Nutze expliziten Start-Datenindex {start_data_index} (Sheet Zeile {args.start_row}) für 'full_run'.") + else: + logging.info("Ermittle Startindex für 'full_run' (erste Zeile ohne AO)...") + start_data_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung") + + if start_data_index != -1 and start_data_index < len(sheet_handler.get_data()): + num_available = len(sheet_handler.get_data()) - start_data_index + num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available + if num_to_process > 0: + logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.") + # Übergebe Flags an process_rows_sequentially + data_processor.process_rows_sequentially( + start_data_index, + num_to_process, + process_wiki=True, + process_chatgpt=True, + process_website=True + ) # Nutzt jetzt logging intern + else: + logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).") + else: + logging.warning(f"Startindex {start_data_index} für 'full_run' ungültig oder keine Zeilen mehr.") + elif mode == "alignment": - print("\nACHTUNG: Überschreibt A1:AX5!"); # AX statt AS - try: confirm = input("Fortfahren? (j/N): ").strip().lower() - except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n' - if confirm == 'j': alignment_demo(sheet_handler.sheet) - else: print("Abgebrochen.") + print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!") + try: + confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower() + except Exception as e_input: + logging.error(f"Input-Fehler bei Bestätigung: {e_input}") + confirm = 'n' + if confirm == 'j': + logging.info("Starte Alignment Demo...") + alignment_demo(sheet_handler.sheet) # Annahme: nutzt logging intern + else: + logging.info("Alignment Demo abgebrochen.") - # --- NEU: Wiki Update Modus --- + # --- Wiki Update Modus --- elif mode == "update_wiki": - process_wiki_updates_from_chatgpt(sheet_handler, data_processor) - # --- Ende Wiki Update Modus --- + logging.info("Starte Modus 'update_wiki'...") + process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit) # Limit optional übergeben - # Block für Modelltraining (wie von dir bereitgestellt) + # Block für Modelltraining elif mode == "train_technician_model": - debug_print(f"Starte Modus: {mode}") + logging.info(f"Starte Modus: {mode}") + # (Dieser Block verwendet jetzt logging intern, keine Anpassungen hier nötig, + # außer sicherzustellen, dass prepare_data_for_modeling logging nutzt) prepared_df = data_processor.prepare_data_for_modeling() if prepared_df is not None and not prepared_df.empty: - debug_print("Aufteilen der Daten...") + logging.info("Aufteilen der Daten für das Modelltraining...") try: + # Definition von X und y (wie im Original, Annahme: prepare_data... hat Spalten 'name' etc.) X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric']) y = prepared_df['Techniker_Bucket'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y) split_successful = True - except Exception as e: debug_print(f"FEHLER Split: {e}"); split_successful = False + logging.info(f"Train/Test Split: {len(X_train)} Train, {len(X_test)} Test samples.") + except Exception as e: + logging.error(f"FEHLER beim Train/Test Split: {e}") + split_successful = False + if split_successful: - debug_print("Imputation...") + logging.info("Imputation fehlender numerischer Werte (Median)...") numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] try: imputer = SimpleImputer(strategy='median') - X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) - X_test[numeric_features] = imputer.transform(X_test[numeric_features]) - imputer_filename = args.imputer_out; pickle.dump(imputer, open(imputer_filename, 'wb')) - debug_print(f"Imputer gespeichert: '{imputer_filename}'.") - imputation_successful = True - except Exception as e: debug_print(f"FEHLER Imputation: {e}"); imputation_successful = False + # Stelle sicher, dass Spalten existieren bevor Transformation + if all(nf in X_train.columns for nf in numeric_features): + X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) + X_test[numeric_features] = imputer.transform(X_test[numeric_features]) + imputer_filename = args.imputer_out + with open(imputer_filename, 'wb') as f_imp: pickle.dump(imputer, f_imp) + logging.info(f"Imputer erfolgreich trainiert und gespeichert: '{imputer_filename}'.") + imputation_successful = True + else: + logging.error("FEHLER: Numerische Features für Imputation nicht in Trainingsdaten gefunden.") + imputation_successful = False + except Exception as e: + logging.error(f"FEHLER bei der Imputation: {e}") + imputation_successful = False + if imputation_successful: - debug_print("Starte Training/GridSearchCV...") - param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15], 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30], 'ccp_alpha': [0.0, 0.001, 0.005]} + logging.info("Starte Decision Tree Training mit GridSearchCV...") + # Parameter Grid (wie im Original) + param_grid = { + 'criterion': ['gini', 'entropy'], + 'max_depth': [6, 8, 10, 12, 15], + 'min_samples_split': [20, 40, 60], + 'min_samples_leaf': [10, 20, 30], + 'ccp_alpha': [0.0, 0.001, 0.005] # Cost-Complexity Pruning + } dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') - grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='f1_weighted', n_jobs=-1, verbose=1) + # GridSearchCV (wie im Original) + grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, + scoring='f1_weighted', n_jobs=-1, verbose=1) try: - grid_search.fit(X_train, y_train) - best_estimator = grid_search.best_estimator_ - debug_print(f"GridSearchCV fertig. Beste Params: {grid_search.best_params_}, Score: {grid_search.best_score_:.4f}") - model_filename = args.model_out; pickle.dump(best_estimator, open(model_filename, 'wb')) - debug_print(f"Modell gespeichert: '{model_filename}'.") - training_successful = True - except Exception as e_train: debug_print(f"FEHLER Training: {e_train}"); training_successful = False; import traceback; debug_print(traceback.format_exc()) + grid_search.fit(X_train, y_train) + best_estimator = grid_search.best_estimator_ + logging.info(f"GridSearchCV abgeschlossen.") + logging.info(f"Beste Parameter: {grid_search.best_params_}") + logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}") + + # Speichere das beste Modell + model_filename = args.model_out + with open(model_filename, 'wb') as f_mod: pickle.dump(best_estimator, f_mod) + logging.info(f"Bestes Modell gespeichert: '{model_filename}'.") + training_successful = True + except Exception as e_train: + logging.error(f"FEHLER während des Trainings: {e_train}") + logging.exception("Traceback Training:") # Loggt Traceback + training_successful = False + if training_successful: - debug_print("Evaluiere Test-Set..."); y_pred = best_estimator.predict(X_test) - test_accuracy = accuracy_score(y_test, y_pred) - report = classification_report(y_test, y_pred, zero_division=0, labels=best_estimator.classes_, target_names=best_estimator.classes_) - conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_) - conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_) - debug_print(f"\n--- Evaluation Test-Set ---\nGenauigkeit: {test_accuracy:.4f}\nBericht:\n{report}\nMatrix:\n{conf_matrix_df}"); print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") - debug_print("\nExtrahiere Regeln..."); + logging.info("Evaluiere Modell auf dem Test-Set...") try: - feature_names = list(X_train.columns); class_names = best_estimator.classes_ - rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3) - patterns_filename = args.patterns_out; - with open(patterns_filename, 'w', encoding='utf-8') as f: f.write(rules_text) - debug_print(f"Regeln gespeichert: '{patterns_filename}'.") - except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}") - else: debug_print("Datenvorbereitung fehlgeschlagen -> Abbruch ML Training.") + y_pred = best_estimator.predict(X_test) + test_accuracy = accuracy_score(y_test, y_pred) + report = classification_report(y_test, y_pred, zero_division=0, + labels=best_estimator.classes_, target_names=best_estimator.classes_) + conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_) + conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_) + + logging.info(f"\n--- Evaluation Test-Set ---") + logging.info(f"Genauigkeit: {test_accuracy:.4f}") + logging.info(f"Classification Report:\n{report}") + logging.info(f"Confusion Matrix:\n{conf_matrix_df}") + print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") # Auch auf Konsole + + logging.info("Extrahiere Baumregeln...") + try: + feature_names = list(X_train.columns) + class_names = list(best_estimator.classes_) # Sicherstellen, dass es eine Liste ist + rules_text = export_text(best_estimator, feature_names=feature_names, + show_weights=True, spacing=3) # class_names optional + patterns_filename = args.patterns_out + with open(patterns_filename, 'w', encoding='utf-8') as f_rules: + f_rules.write(rules_text) + logging.info(f"Regeln als Text gespeichert: '{patterns_filename}'.") + except Exception as e_export: + logging.error(f"Fehler beim Exportieren der Regeln: {e_export}") + except Exception as e_eval: + logging.error(f"Fehler bei der Evaluation des Test-Sets: {e_eval}") + else: + logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.") else: - debug_print(f"Unbekannter Modus '{mode}'.") + logging.error(f"Unbekannter Modus '{mode}' wurde zur Ausführung übergeben.") + except KeyboardInterrupt: + logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).") + print("\n! Skript wurde manuell beendet.") except Exception as e: - debug_print(f"FATAL: Unerwarteter Fehler in main try-Block: {e}") - import traceback; debug_print(traceback.format_exc()) + # Fange alle unerwarteten Fehler im Hauptblock ab + logging.critical(f"FATAL: Unerwarteter Fehler im Haupt-Ausführungsblock des Modus '{mode}': {e}") + logging.exception("Traceback des kritischen Fehlers:") # Loggt den Traceback # --- Abschluss --- - end_time = time.time(); duration = end_time - start_time - debug_print(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.") - debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.") - debug_print(f"===== Skript beendet =====") - if LOG_FILE: - try: - # 'with' startet in der nächsten Zeile - with open(LOG_FILE, "a", encoding="utf-8") as f: - f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ===== Skript wirklich beendet =====\n") - except Exception as e: - # Optional: Warnung ausgeben, wenn das finale Schreiben fehlschlägt - print(f"[WARNUNG] Konnte letzte Log-Nachricht nicht schreiben: {e}") - pass # Programm soll trotzdem normal beenden - # --- ENDE KORRIGIERTER BLOCK --- + end_time = time.time() + duration = end_time - start_time + logging.info(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.") + logging.info(f"Gesamtdauer: {duration:.2f} Sekunden.") + logging.info(f"===== Skript beendet =====") - print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}") + # Schließe Logging Handler explizit (optional, aber sauber) + logging.shutdown() + + print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}") # Führt die main-Funktion aus, wenn das Skript direkt gestartet wird