This commit is contained in:
2025-04-19 17:23:36 +00:00
parent 6cf9afca87
commit a69a3594ab

View File

@@ -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