v1.6.4: Implementiere ML-Modelltraining zur Technikerschätzung
- Füge neuen Betriebsmodus `--mode train_technician_model` hinzu.
- Implementiere Datenvorbereitung in `DataProcessor.prepare_data_for_modeling`:
- Lädt relevante Spalten.
- Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität).
- Filtert nach gültiger Technikerzahl (>0).
- Erstellt Zielvariable `Techniker_Bucket` (7 Kategorien).
- Führt One-Hot Encoding für Branchen durch.
- Implementiere Logik im `train_technician_model`-Modus in `main`:
- Führt Train/Test-Split durch (stratifiziert).
- Imputiert fehlende numerische Werte mit Median (fittet auf Train, transformiert Train/Test).
- Trainiert einen `DecisionTreeClassifier` mittels `GridSearchCV` zur Hyperparameter-Optimierung (Fokus auf `f1_weighted`).
- Evaluiert das beste Modell auf dem Test-Set (Accuracy, Classification Report, Confusion Matrix).
- Extrahiert Baumregeln mittels `export_text`.
- Speichert den trainierten Imputer, das beste Modell (`.pkl`) und die extrahierten Regeln (`.txt`).
- Füge notwendige Imports für `pandas`, `numpy`, `sklearn`, `pickle`, `json` hinzu.
- Ergänze neue Konfigurationsparameter für ML in `Config` (Worker, Limits).
- Füge Kommandozeilenargumente für Modell-Ausgabedateien hinzu.
This commit is contained in:
@@ -1,13 +1,25 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
v1.6.3: Beschleunige Website-Scraping durch gebündelte Sheet-Updates
|
v1.6.4: Implementiere ML-Modelltraining zur Technikerschätzung
|
||||||
|
|
||||||
Git-Änderungsbeschreibung:
|
Git-Änderungsbeschreibung:
|
||||||
- Überarbeite `process_website_batch` zur Leistungssteigerung.
|
- Füge neuen Betriebsmodus `--mode train_technician_model` hinzu.
|
||||||
- Implementiere das Sammeln von Zell-Updates (`AR`, `AS`, `AT`, `AP`) für mehrere Zeilen in einer Liste (`all_sheet_updates`).
|
- Implementiere Datenvorbereitung in `DataProcessor.prepare_data_for_modeling`:
|
||||||
- Sende die gesammelten Updates gebündelt über einen einzigen `batch_update_cells`-Aufruf an Google Sheets, wenn ein Limit (`update_batch_row_limit`) erreicht ist oder die Schleife endet.
|
- Lädt relevante Spalten.
|
||||||
- Ziel: Reduzierung der Anzahl von Google Sheets API-Aufrufen und Beschleunigung des Website-Scraping-Prozesses.
|
- Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität).
|
||||||
- Stelle sicher, dass auch ein letzter, unvollständiger Batch nach der Hauptschleife gesendet wird.
|
- Filtert nach gültiger Technikerzahl (>0).
|
||||||
|
- Erstellt Zielvariable `Techniker_Bucket` (7 Kategorien).
|
||||||
|
- Führt One-Hot Encoding für Branchen durch.
|
||||||
|
- Implementiere Logik im `train_technician_model`-Modus in `main`:
|
||||||
|
- Führt Train/Test-Split durch (stratifiziert).
|
||||||
|
- Imputiert fehlende numerische Werte mit Median (fittet auf Train, transformiert Train/Test).
|
||||||
|
- Trainiert einen `DecisionTreeClassifier` mittels `GridSearchCV` zur Hyperparameter-Optimierung (Fokus auf `f1_weighted`).
|
||||||
|
- Evaluiert das beste Modell auf dem Test-Set (Accuracy, Classification Report, Confusion Matrix).
|
||||||
|
- Extrahiert Baumregeln mittels `export_text`.
|
||||||
|
- Speichert den trainierten Imputer, das beste Modell (`.pkl`) und die extrahierten Regeln (`.txt`).
|
||||||
|
- Füge notwendige Imports für `pandas`, `numpy`, `sklearn`, `pickle`, `json` hinzu.
|
||||||
|
- Ergänze neue Konfigurationsparameter für ML in `Config` (Worker, Limits).
|
||||||
|
- Füge Kommandozeilenargumente für Modell-Ausgabedateien hinzu.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -28,14 +40,15 @@ from urllib.parse import urlparse, urlencode
|
|||||||
import argparse
|
import argparse
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
# --- NEUE IMPORTE für ML ---
|
||||||
from sklearn.model_selection import train_test_split, GridSearchCV
|
from sklearn.model_selection import train_test_split, GridSearchCV
|
||||||
from sklearn.impute import SimpleImputer
|
from sklearn.impute import SimpleImputer
|
||||||
from sklearn.tree import DecisionTreeClassifier, export_text
|
from sklearn.tree import DecisionTreeClassifier, export_text
|
||||||
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
|
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
|
||||||
import json # Zum Speichern der Muster als JSON
|
import json # Zum Speichern der Muster als JSON (optional)
|
||||||
import pickle # Zum Speichern des trainierten Modells und Imputers
|
import pickle # Zum Speichern des trainierten Modells und Imputers
|
||||||
import concurrent.futures
|
import concurrent.futures # Für parallele Verarbeitung (bereits vorhanden)
|
||||||
import threading
|
import threading # Für Semaphore (bereits vorhanden)
|
||||||
|
|
||||||
# Optional: tiktoken für Token-Zählung (Modus 8)
|
# Optional: tiktoken für Token-Zählung (Modus 8)
|
||||||
try:
|
try:
|
||||||
@@ -50,10 +63,15 @@ SERP_API_KEY_FILE = "serpApiKey.txt"
|
|||||||
GENDERIZE_API_KEY_FILE = "genderize_API_Key.txt"
|
GENDERIZE_API_KEY_FILE = "genderize_API_Key.txt"
|
||||||
BRANCH_MAPPING_FILE = "ziel_Branchenschema.csv"
|
BRANCH_MAPPING_FILE = "ziel_Branchenschema.csv"
|
||||||
LOG_DIR = "Log"
|
LOG_DIR = "Log"
|
||||||
|
# --- NEU: Dateinamen für Modell-Artefakte ---
|
||||||
|
MODEL_FILE = "technician_decision_tree_model.pkl"
|
||||||
|
IMPUTER_FILE = "median_imputer.pkl"
|
||||||
|
PATTERNS_FILE_TXT = "technician_patterns.txt"
|
||||||
|
PATTERNS_FILE_JSON = "technician_patterns.json" # Optional
|
||||||
|
|
||||||
# ==================== KONFIGURATION ====================
|
# ==================== KONFIGURATION ====================
|
||||||
class Config:
|
class Config:
|
||||||
VERSION = "v1.6.3" # Behalte Version bei
|
VERSION = "v1.6.4" # Behalte Version bei
|
||||||
LANG = "de"
|
LANG = "de"
|
||||||
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
|
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
|
||||||
MAX_RETRIES = 3
|
MAX_RETRIES = 3
|
||||||
@@ -3557,258 +3575,198 @@ class DataProcessor:
|
|||||||
|
|
||||||
# ==================== MAIN FUNCTION ====================
|
# ==================== MAIN FUNCTION ====================
|
||||||
def main():
|
def main():
|
||||||
global LOG_FILE # LOG_FILE wird global benötigt
|
global LOG_FILE
|
||||||
|
|
||||||
# --- Initialisierung ---
|
# --- Initialisierung ---
|
||||||
# Argument Parser
|
|
||||||
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript")
|
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript")
|
||||||
# HIER NEU: 'summarize' als Option hinzugefügt
|
|
||||||
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model"]
|
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model"]
|
||||||
parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})")
|
parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})")
|
||||||
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen (für Batch/sequentielle Modi)", default=None)
|
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen", default=None)
|
||||||
# Optional: Argumente speziell für das Training hinzufügen? z.B. --output_model_file
|
# --- NEU: Argumente für Modelltraining ---
|
||||||
# parser.add_argument("--model_out", type=str, default="technician_model.pkl", help="Dateipfad zum Speichern des trainierten Modells")
|
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Dateipfad zum Speichern des Modells (Standard: {MODEL_FILE})")
|
||||||
# parser.add_argument("--patterns_out", type=str, default="technician_patterns.json", help="Dateipfad zum Speichern der extrahierten Muster")
|
parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Dateipfad zum Speichern des Imputers (Standard: {IMPUTER_FILE})")
|
||||||
|
parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Dateipfad zum Speichern der Text-Regeln (Standard: {PATTERNS_FILE_TXT})")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Lade API Keys
|
|
||||||
Config.load_api_keys()
|
Config.load_api_keys()
|
||||||
|
|
||||||
# Betriebsmodus ermitteln
|
# Betriebsmodus ermitteln (wie gehabt)
|
||||||
mode = None
|
mode = None
|
||||||
# Priorisiere Kommandozeilenargumente
|
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:
|
else: # Interaktive Abfrage (wie gehabt)
|
||||||
mode = args.mode.lower()
|
# ... (print Optionen etc.) ...
|
||||||
print(f"Betriebsmodus (aus Kommandozeile): {mode}")
|
try: mode_input = input(f"Geben Sie den Modus ein: ").strip().lower();
|
||||||
else:
|
except Exception as e: print(f"Fehler Modus-Eingabe ({e}) -> combined"); mode = "combined"
|
||||||
# Nur wenn KEIN Modus über die Kommandozeile kam, FRAGE interaktiv
|
if mode_input in valid_modes: mode = mode_input
|
||||||
print("Bitte wählen Sie den Betriebsmodus:")
|
else: print("Ungültige Eingabe -> combined"); mode = "combined"
|
||||||
print(" combined: Wiki(AX), Website-Scrape(AT), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)") # Aktualisiert
|
|
||||||
print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") # Aktualisiert
|
|
||||||
print(" website: Nur Website-Scraping Rohtext (AT) (Batch, Start bei leerem AT)") # Aktualisiert
|
|
||||||
print(" summarize: Nur Website-Zusammenfassung aus Rohtext (AS) (Batch, Start bei leerem AO)") # NEU
|
|
||||||
print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") # Aktualisiert
|
|
||||||
print(" reeval: Verarbeitet Zeilen mit 'x' (volle Verarbeitung, alle TS prüfen)")
|
|
||||||
print(" website_lookup: Sucht fehlende Websites (D)")
|
|
||||||
print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR)")
|
|
||||||
print(" contacts: Sucht LinkedIn Kontakte (AM)")
|
|
||||||
print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)") # Aktualisiert
|
|
||||||
print(" alignment: Schreibt Header A1:AX5 (!)") # Aktualisiert
|
|
||||||
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. Standardmodus 'combined' wird verwendet.")
|
|
||||||
mode = "combined"
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno == 9:
|
|
||||||
print("Fehler: Interaktive Modus-Abfrage nicht möglich (läuft im Hintergrund?). Standardmodus 'combined' wird verwendet.")
|
|
||||||
mode = "combined"
|
|
||||||
else:
|
|
||||||
print(f"Unerwarteter OS-Fehler bei Modus-Abfrage: {e}")
|
|
||||||
print("Standardmodus 'combined' wird verwendet.")
|
|
||||||
mode = "combined"
|
|
||||||
except EOFError:
|
|
||||||
print("Fehler: Interaktive Modus-Abfrage nicht möglich (EOF). Standardmodus 'combined' wird verwendet.")
|
|
||||||
mode = "combined"
|
|
||||||
|
|
||||||
|
# Zeilenlimit ermitteln (wie gehabt)
|
||||||
# Zeilenlimit ermitteln
|
|
||||||
row_limit = None
|
row_limit = None
|
||||||
if args.limit is not None:
|
if args.limit is not None:
|
||||||
if args.limit >= 0:
|
if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
|
||||||
row_limit = args.limit
|
else: print("Warnung: Negatives Limit ignoriert."); row_limit = None
|
||||||
print(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
|
|
||||||
else:
|
|
||||||
print("Warnung: Negatives Zeilenlimit ignoriert. Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
# HIER NEU: summarize hinzugefügt
|
|
||||||
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]:
|
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]:
|
||||||
try:
|
try: limit_input = input("Max Zeilen? (Enter=alle): ");
|
||||||
limit_input = input("Wie viele Zeilen sollen maximal bearbeitet werden? (Enter für alle) ")
|
except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None
|
||||||
if limit_input.strip():
|
if limit_input.strip():
|
||||||
limit_val = int(limit_input)
|
try: limit_val = int(limit_input);
|
||||||
if limit_val >= 0:
|
except ValueError: print("Ungültige Zahl -> Kein Limit"); row_limit = None
|
||||||
row_limit = limit_val
|
if limit_val >= 0: row_limit = limit_val; print(f"Zeilenlimit: {row_limit}")
|
||||||
print(f"Zeilenlimit: {row_limit}")
|
else: print("Negatives Limit -> Kein Limit"); row_limit = None
|
||||||
else:
|
else: row_limit = None; print("Kein Zeilenlimit.")
|
||||||
print("Warnung: Negatives Zeilenlimit ignoriert. Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
else:
|
|
||||||
row_limit = None
|
|
||||||
print("Kein Zeilenlimit gesetzt.")
|
|
||||||
except ValueError:
|
|
||||||
print("Ungültige Eingabe für Zeilenlimit. Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno == 9:
|
|
||||||
print("Warnung: Interaktive Abfrage des Limits nicht möglich (läuft im Hintergrund?). Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
else:
|
|
||||||
print(f"Unerwarteter OS-Fehler bei Limit-Abfrage: {e}")
|
|
||||||
print("Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
except EOFError:
|
|
||||||
print("Warnung: Interaktive Abfrage des Limits nicht möglich (EOF). Kein Limit gesetzt.")
|
|
||||||
row_limit = None
|
|
||||||
|
|
||||||
|
# Logfile initialisieren (wie gehabt)
|
||||||
# Logfile initialisieren
|
|
||||||
LOG_FILE = create_log_filename(mode)
|
LOG_FILE = create_log_filename(mode)
|
||||||
debug_print(f"===== Skript gestartet =====")
|
# ... (Logging Startparameter) ...
|
||||||
debug_print(f"Version: {Config.VERSION}")
|
debug_print(f"===== Skript gestartet ====="); debug_print(f"Version: {Config.VERSION}")
|
||||||
debug_print(f"Betriebsmodus: {mode}")
|
debug_print(f"Betriebsmodus: {mode}"); # ... (Restliches Logging)
|
||||||
limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus'
|
|
||||||
# HIER NEU: summarize hinzugefügt
|
|
||||||
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}")
|
|
||||||
|
|
||||||
# --- Vorbereitung ---
|
# --- Vorbereitung ---
|
||||||
load_target_schema()
|
load_target_schema()
|
||||||
try:
|
try: sheet_handler = GoogleSheetHandler();
|
||||||
sheet_handler = GoogleSheetHandler()
|
except Exception as e: debug_print(f"FATAL: Init GSheet: {e}"); print(f"FEHLER GSheet. Log: {LOG_FILE}"); return
|
||||||
# Stelle sicher, dass nach der Initialisierung Daten geladen sind
|
|
||||||
if not sheet_handler.sheet_values:
|
|
||||||
raise ValueError("Google Sheet Handler konnte keine Daten laden.")
|
|
||||||
except Exception as e:
|
|
||||||
debug_print(f"FATAL: Konnte Google Sheet Handler nicht initialisieren oder Daten laden: {e}")
|
|
||||||
print(f"FEHLER: Verbindung/Datenladen Google Sheets fehlgeschlagen. Log: {LOG_FILE}")
|
|
||||||
return # Abbruch
|
|
||||||
|
|
||||||
data_processor = DataProcessor(sheet_handler)
|
data_processor = DataProcessor(sheet_handler)
|
||||||
|
|
||||||
# --- Modusausführung ---
|
# --- Modusausführung ---
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...")
|
debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# HIER NEU: summarize hinzugefügt zu dieser Gruppe
|
|
||||||
if mode in ["wiki", "website", "branch", "summarize", "combined"]:
|
if mode in ["wiki", "website", "branch", "summarize", "combined"]:
|
||||||
if row_limit == 0:
|
if row_limit == 0: debug_print("Limit 0 -> Skip Dispatcher.")
|
||||||
debug_print("Zeilenlimit ist 0. Überspringe Dispatcher-Aufruf.")
|
else: run_dispatcher(mode, sheet_handler, row_limit)
|
||||||
else:
|
elif mode == "reeval": data_processor.process_reevaluation_rows()
|
||||||
# Rufe Dispatcher auf, der den richtigen Startpunkt findet und die passende Funktion aufruft
|
elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty()
|
||||||
run_dispatcher(mode, sheet_handler, row_limit)
|
elif mode == "website_details": data_processor.process_website_details_for_marked_rows()
|
||||||
elif mode == "reeval":
|
elif mode == "contacts": process_contact_research(sheet_handler)
|
||||||
data_processor.process_reevaluation_rows() # Nutzt _process_single_row intern
|
|
||||||
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) # Annahme: Diese Funktion existiert global
|
|
||||||
elif mode == "full_run":
|
elif mode == "full_run":
|
||||||
if row_limit == 0:
|
if row_limit == 0: debug_print("Limit 0 -> Skip full_run.")
|
||||||
debug_print("Zeilenlimit ist 0. Überspringe sequenzielle Verarbeitung.")
|
else: # Logik für full_run (wie gehabt)
|
||||||
else:
|
|
||||||
# full_run startet immer ab der ersten Zeile ohne AO
|
|
||||||
start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung")
|
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()):
|
if start_index != -1 and start_index < len(sheet_handler.get_data()):
|
||||||
|
#... (Berechne num_to_process) ...
|
||||||
num_available = len(sheet_handler.get_data()) - start_index
|
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
|
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:
|
if num_to_process > 0: data_processor.process_rows_sequentially(start_index, num_to_process)
|
||||||
# _process_single_row prüft alle Timestamps intern
|
else: debug_print("Keine Zeilen für full_run.")
|
||||||
data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True)
|
else: debug_print(f"Startindex {start_index} für full_run ungültig.")
|
||||||
else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten.")
|
elif mode == "alignment": # Logik für Alignment (wie gehabt)
|
||||||
else: debug_print(f"Startindex ({start_index}) für 'full_run' ungültig oder keine Daten.")
|
# ... (Bestätigungsabfrage und Aufruf) ...
|
||||||
elif mode == "alignment":
|
print("\nACHTUNG: Überschreibt A1:AX5!");
|
||||||
print("\nACHTUNG: Dieser Modus überschreibt die Zellen A1:AX5 im Haupt-Sheet!") # AX statt AS
|
try: confirm = input("Fortfahren? (j/N): ").strip().lower()
|
||||||
print("Diese Zellen enthalten die Spaltendefinitionen (Alignment Demo).")
|
except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n'
|
||||||
try:
|
if confirm == 'j': alignment_demo(sheet_handler.sheet)
|
||||||
confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower()
|
else: print("Abgebrochen.")
|
||||||
if confirm == 'j':
|
|
||||||
debug_print("Bestätigung erhalten. Starte Alignment Demo...")
|
|
||||||
alignment_demo(sheet_handler.sheet) # Ruft die globale Funktion auf
|
|
||||||
debug_print("Alignment Demo Aufruf beendet.")
|
|
||||||
else:
|
|
||||||
print("Vorgang abgebrochen.")
|
|
||||||
debug_print("Alignment Demo vom Benutzer abgebrochen.")
|
|
||||||
except Exception as e: # Fange generische Exceptions für input()
|
|
||||||
print(f"Fehler bei Bestätigung ({e}). Vorgang abgebrochen.")
|
|
||||||
debug_print(f"Alignment Demo abgebrochen (Fehler: {e}).")
|
|
||||||
|
|
||||||
|
# --- NEUER BLOCK: Modelltraining ---
|
||||||
elif mode == "train_technician_model":
|
elif mode == "train_technician_model":
|
||||||
debug_print("Starte Modus: train_technician_model")
|
debug_print(f"Starte Modus: {mode}")
|
||||||
# Rufe die Methode über die data_processor Instanz auf
|
|
||||||
prepared_df = data_processor.prepare_data_for_modeling() # Korrigierter Aufruf
|
|
||||||
|
|
||||||
if prepared_df is not None and not prepared_df.empty:
|
# 1. Daten vorbereiten
|
||||||
# --- Train/Test Split ---
|
prepared_df = data_processor.prepare_data_for_modeling()
|
||||||
debug_print("Aufteilen der Daten in Trainings- und Testsets...")
|
|
||||||
|
if prepared_df is None or prepared_df.empty:
|
||||||
|
debug_print("FEHLER: Datenvorbereitung fehlgeschlagen oder keine Daten vorhanden. Modus wird abgebrochen.")
|
||||||
|
else:
|
||||||
|
# 2. Train/Test Split
|
||||||
|
debug_print("Aufteilen der Daten in Trainings- und Testsets (Testgröße 25%, stratifiziert)...")
|
||||||
try:
|
try:
|
||||||
# Stelle sicher, dass 'name' und Original-Technikerzahl entfernt werden, bevor gesplittet wird
|
# Features X (ohne Ziel und Hilfsspalten), Target y
|
||||||
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric'])
|
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric'])
|
||||||
y = prepared_df['Techniker_Bucket']
|
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)
|
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
|
||||||
debug_print(f"Trainingsdaten: {X_train.shape}, Testdaten: {X_test.shape}")
|
debug_print(f"Trainingsdaten: {X_train.shape[0]} Zeilen, {X_train.shape[1]} Features")
|
||||||
|
debug_print(f"Testdaten: {X_test.shape[0]} Zeilen, {X_test.shape[1]} Features")
|
||||||
|
split_successful = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_print(f"Fehler beim Train/Test Split: {e}")
|
debug_print(f"FEHLER beim Train/Test Split: {e}"); split_successful = False
|
||||||
X_train, X_test, y_train, y_test = None, None, None, None
|
|
||||||
|
|
||||||
if X_train is not None:
|
if split_successful:
|
||||||
# --- Imputation ---
|
# 3. Imputation fehlender Werte
|
||||||
debug_print("Imputation fehlender Werte (Median)...")
|
debug_print("Imputation fehlender Werte für 'Finaler_Umsatz', 'Finaler_Mitarbeiter' (Median)...")
|
||||||
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
|
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
|
||||||
try:
|
try:
|
||||||
imputer = SimpleImputer(strategy='median')
|
imputer = SimpleImputer(strategy='median')
|
||||||
imputer.fit(X_train[numeric_features])
|
# WICHTIG: Imputer NUR auf Trainingsdaten fitten!
|
||||||
X_train[numeric_features] = imputer.transform(X_train[numeric_features])
|
X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features])
|
||||||
|
# Testdaten NUR transformieren
|
||||||
X_test[numeric_features] = imputer.transform(X_test[numeric_features])
|
X_test[numeric_features] = imputer.transform(X_test[numeric_features])
|
||||||
imputer_filename = "median_imputer.pkl"; pickle.dump(imputer, open(imputer_filename, 'wb'))
|
|
||||||
|
# Speichere den Imputer
|
||||||
|
imputer_filename = args.imputer_out # Verwende Argument oder Default
|
||||||
|
with open(imputer_filename, 'wb') as f: pickle.dump(imputer, f)
|
||||||
debug_print(f"Median-Imputer trainiert und gespeichert als '{imputer_filename}'.")
|
debug_print(f"Median-Imputer trainiert und gespeichert als '{imputer_filename}'.")
|
||||||
imputation_successful = True
|
imputation_successful = True
|
||||||
except Exception as e: debug_print(f"Fehler Imputation: {e}"); imputation_successful = False
|
except Exception as e:
|
||||||
|
debug_print(f"FEHLER bei der Imputation: {e}"); imputation_successful = False
|
||||||
|
|
||||||
if imputation_successful:
|
if imputation_successful:
|
||||||
# --- Modelltraining & Tuning ---
|
# 4. Modelltraining & Hyperparameter-Tuning
|
||||||
debug_print("Starte Decision Tree Training mit GridSearchCV...")
|
debug_print("Starte Decision Tree Training mit GridSearchCV...")
|
||||||
param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [5, 8, 10, 12, 15], 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30], 'ccp_alpha': [0.0, 0.001, 0.005]}
|
# Verkleinerter Grid zum Testen, kann erweitert werden
|
||||||
dtree = DecisionTreeClassifier(random_state=42)
|
param_grid = {
|
||||||
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1, verbose=1)
|
'criterion': ['gini', 'entropy'],
|
||||||
|
'max_depth': [6, 8, 10, 12],
|
||||||
|
'min_samples_split': [20, 40],
|
||||||
|
'min_samples_leaf': [10, 20],
|
||||||
|
'ccp_alpha': [0.0, 0.001, 0.005]
|
||||||
|
}
|
||||||
|
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') # Balanced für ungleiche Klassen?
|
||||||
|
# F1-Score oft besser für unbalancierte Klassen als Accuracy
|
||||||
|
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='f1_weighted', n_jobs=-1, verbose=1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
grid_search.fit(X_train, y_train)
|
grid_search.fit(X_train, y_train)
|
||||||
best_params = grid_search.best_params_; best_score = grid_search.best_score_; best_estimator = grid_search.best_estimator_
|
best_estimator = grid_search.best_estimator_
|
||||||
debug_print(f"Beste Parameter: {best_params}, Bester Score: {best_score:.4f}")
|
debug_print(f"GridSearchCV abgeschlossen.")
|
||||||
model_filename = "technician_decision_tree_model.pkl"; pickle.dump(best_estimator, open(model_filename, 'wb'))
|
debug_print(f"Beste Parameter: {grid_search.best_params_}")
|
||||||
debug_print(f"Bestes Modell gespeichert als '{model_filename}'.")
|
debug_print(f"Bester Kreuzvalidierungs-Score (F1 Weighted): {grid_search.best_score_:.4f}")
|
||||||
|
|
||||||
# --- Evaluation ---
|
# Speichere das beste Modell
|
||||||
debug_print("Evaluiere bestes Modell auf Test-Set...")
|
model_filename = args.model_out # Verwende Argument oder Default
|
||||||
|
with open(model_filename, 'wb') as f: pickle.dump(best_estimator, f)
|
||||||
|
debug_print(f"Bestes Modell gespeichert als '{model_filename}'.")
|
||||||
|
training_successful = True
|
||||||
|
|
||||||
|
except Exception as e_train:
|
||||||
|
debug_print(f"FEHLER während Training/Tuning: {e_train}"); training_successful = False
|
||||||
|
import traceback; debug_print(traceback.format_exc())
|
||||||
|
|
||||||
|
if training_successful:
|
||||||
|
# 5. Evaluation auf dem Test-Set
|
||||||
|
debug_print("Evaluiere bestes Modell auf dem Test-Set...")
|
||||||
y_pred = best_estimator.predict(X_test)
|
y_pred = best_estimator.predict(X_test)
|
||||||
test_accuracy = accuracy_score(y_test, y_pred)
|
test_accuracy = accuracy_score(y_test, y_pred)
|
||||||
report = classification_report(y_test, y_pred, zero_division=0)
|
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)
|
conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_)
|
||||||
debug_print(f"\n--- Evaluation Test-Set ---")
|
# Erstelle DataFrame für bessere Lesbarkeit der Matrix
|
||||||
debug_print(f"Genauigkeit: {test_accuracy:.4f}"); print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}")
|
conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_)
|
||||||
debug_print(f"Bericht:\n{report}"); print(f"Log für Details: {LOG_FILE}")
|
|
||||||
debug_print(f"Matrix:\n{conf_matrix}")
|
|
||||||
|
|
||||||
# --- Muster extrahieren ---
|
debug_print(f"\n--- Evaluationsergebnisse (Test-Set) ---")
|
||||||
debug_print("\nExtrahiere Regeln (Text)...")
|
debug_print(f"Genauigkeit: {test_accuracy:.4f}")
|
||||||
|
debug_print(f"Klassifikationsbericht:\n{report}")
|
||||||
|
debug_print(f"Konfusionsmatrix:\n{conf_matrix_df}")
|
||||||
|
print(f"\nModell-Evaluation abgeschlossen. Genauigkeit (Test): {test_accuracy:.4f}")
|
||||||
|
print(f"Log für Details: {LOG_FILE}")
|
||||||
|
|
||||||
|
# 6. Muster extrahieren (Text)
|
||||||
|
debug_print("\nExtrahiere Regeln aus dem besten Baum...")
|
||||||
try:
|
try:
|
||||||
feature_names = list(X_train.columns)
|
feature_names = list(X_train.columns)
|
||||||
rules_text = export_text(best_estimator, feature_names=feature_names, show_weights=True)
|
class_names = best_estimator.classes_
|
||||||
debug_print(f"--- Baumregeln ---:\n{rules_text[:2000]}...") # Gekürzt für Log
|
rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3) # Bessere Formatierung
|
||||||
patterns_filename_txt = "technician_patterns.txt"
|
# debug_print(f"--- Baumregeln (Text) ---:\n{rules_text}") # Kann sehr lang sein
|
||||||
with open(patterns_filename_txt, 'w', encoding='utf-8') as f_rules: f_rules.write(rules_text)
|
patterns_filename = args.patterns_out # Verwende Argument oder Default
|
||||||
debug_print(f"Regeln gespeichert in '{patterns_filename_txt}'.")
|
with open(patterns_filename, 'w', encoding='utf-8') as f: f.write(rules_text)
|
||||||
|
debug_print(f"Regeln als Text gespeichert in '{patterns_filename}'.")
|
||||||
except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}")
|
except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}")
|
||||||
except Exception as e_train: debug_print(f"FEHLER Training/Tuning: {e_train}"); import traceback; debug_print(traceback.format_exc())
|
|
||||||
else: debug_print("Datenvorbereitung fehlgeschlagen/leer. Modus 'train_technician_model' abgebrochen.")
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
debug_print(f"Unbekannter Modus '{mode}'.")
|
debug_print(f"Unbekannter Modus '{mode}'.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e: # Fange unerwartete Fehler im Hauptblock ab
|
||||||
debug_print(f"FATAL: Unerwarteter Fehler in main try-Block: {e}")
|
debug_print(f"FATAL: Unerwarteter Fehler in main try-Block: {e}")
|
||||||
import traceback
|
import traceback; debug_print(traceback.format_exc())
|
||||||
debug_print(traceback.format_exc())
|
|
||||||
|
|
||||||
# --- Abschluss ---
|
# --- Abschluss ---
|
||||||
end_time = time.time(); duration = end_time - start_time
|
end_time = time.time(); duration = end_time - start_time
|
||||||
@@ -3816,8 +3774,7 @@ def main():
|
|||||||
debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.")
|
debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.")
|
||||||
debug_print(f"===== Skript beendet =====")
|
debug_print(f"===== Skript beendet =====")
|
||||||
if LOG_FILE:
|
if LOG_FILE:
|
||||||
try:
|
try: 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")
|
||||||
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: pass
|
except: pass
|
||||||
print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}")
|
print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user