This commit is contained in:
2025-06-02 14:06:07 +00:00
parent 62d341d309
commit 8dfbaafabf

View File

@@ -9246,73 +9246,41 @@ class DataProcessor:
# train_test_split, SimpleImputer, DecisionTreeClassifier,
# accuracy_score, classification_report, confusion_matrix, export_text (sklearn).
def train_technician_model(self, model_out=MODEL_FILE, imputer_out=IMPUTER_FILE, patterns_out=PATTERNS_FILE_JSON):
"""
Trainiert ein Decision Tree Modell zur Schaetzung der Servicetechniker-Buckets.
Speichert das Modell, den Imputer und die Feature-Spalten.
self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...")
Args:
model_out (str): Dateipfad zum Speichern des trainierten Modells (.pkl).
imputer_out (str): Dateipfad zum Speichern des trainierten Imputers (.pkl).
patterns_out (str): Dateipfad zum Speichern der Feature-Spaltenliste (.json).
"""
# Verwenden Sie logger, da das Logging jetzt konfiguriert ist
self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...") # <<< GEÄNDERT
# 1. Daten vorbereiten (nutzt die interne Methode prepare_data_for_modeling denselben Block)
# 1. Daten vorbereiten
df_model_ready = self.prepare_data_for_modeling()
# Wenn die Datenvorbereitung fehlschlug oder keinen DataFrame zurueckgab
if df_model_ready is None or df_model_ready.empty:
self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.") # <<< GEÄNDERT
return # Beende die Methode
self.logger.error("Datenvorbereitung fuer Modelltraining fehlgeschlagen oder keine Daten. Training abgebrochen.")
return
# Separate Features (X) und Target (y)
# Identifikationsspalten und Zielspalte (muss konsistent mit prepare_data_for_modeling sein)
# Feature Spalten und Zielspalte definieren
identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric']
target_column = 'Techniker_Bucket'
feature_columns_ml = [col for col in df_model_ready.columns if col not in identification_cols and col != target_column]
if not feature_columns_ml:
self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.")
return
# Feature Spalten sind alle Spalten im df_model_ready ausser den Identifikations- und der Zielspalte.
feature_columns = [col for col in df_model_ready.columns if col not in identification_cols and col != target_column]
# Stellen Sie sicher, dass es Feature-Spalten gibt (sollte durch prepare_data_for_modeling sichergestellt sein)
if not feature_columns:
self.logger.critical("FEHLER: Keine Feature-Spalten nach Datenvorbereitung gefunden. Training nicht moeglich.") # <<< GEÄNDERT
return # Beende die Methode
# Erstellen Sie die Feature-Matrix X und den Zielvektor y
X = df_model_ready[feature_columns]
X = df_model_ready[feature_columns_ml]
y = df_model_ready[target_column]
self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") # <<< GEÄNDERT
# Logge die ersten paar Features auf Debug-Level (kann sehr lang sein)
# self.logger.debug(f"Feature Spalten fuer Training ({len(feature_columns)}): {feature_columns[:10]}...")
self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}")
# 2. Split in Training und Test Set
# test_size (z.B. 0.25 für 25% Testdaten), random_state fuer Reproduzierbarkeit.
# stratify=y ist wichtig bei Klassifikationsproblemen mit ungleichen Klassen, um die
# Klassenverteilung in Trainings- und Testset aehnlich zu halten.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.") # <<< GEÄNDERT
self.logger.info(f"Daten gesplittet. Train Set: {len(X_train)} Zeilen, Test Set: {len(X_test)} Zeilen.")
# 3. Imputation (Fehlende Werte ersetzen)
# Verwenden Sie SimpleImputer (z.B. Median), um NaN-Werte zu ersetzen.
# Median ist robust gegenueber Ausreissern. Alternativ: 'mean' oder 'most_frequent'.
imputer = SimpleImputer(strategy='median')
self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...") # <<< GEÄNDERT
# Fitten Sie den Imputer NUR auf den Trainingsdaten, um Data Leakage zu vermeiden.
imputer.fit(X_train) # Fitten Sie den Imputer auf X_train
self.logger.info(f"Fitte Imputer mit Strategie '{imputer.strategy}' auf Trainingsdaten...")
imputer.fit(X_train)
# Speichern Sie den Imputer (wird fuer Vorhersagen benoetigt).
self.imputer = imputer
self.imputer = imputer # Speichern Sie ihn in der Instanz
try:
imputer_dir = os.path.dirname(imputer_out)
if imputer_dir and not os.path.exists(imputer_dir): # Nur erstellen, wenn dirname nicht leer ist
if imputer_dir and not os.path.exists(imputer_dir):
os.makedirs(imputer_dir, exist_ok=True)
with open(imputer_out, 'wb') as f:
pickle.dump(imputer, f)
@@ -9320,69 +9288,87 @@ class DataProcessor:
except Exception as e:
self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}")
self.logger.debug(traceback.format_exc())
# Training sollte hier nicht unbedingt abbrechen, aber ein Hinweis ist wichtig
# Speichern Sie das trainierte Modell
self.model = dt_classifier
X_train_imputed = imputer.transform(X_train)
X_test_imputed = imputer.transform(X_test)
X_train_imputed = pd.DataFrame(X_train_imputed, columns=feature_columns_ml) # feature_columns_ml verwenden
X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns_ml) # feature_columns_ml verwenden
# Die Zeile "Numerische Features imputiert." war ein allgemeiner Kommentar.
# Wichtig ist, dass X_train_imputed und X_test_imputed jetzt bereitstehen.
# 4. Decision Tree Training
dt_classifier = DecisionTreeClassifier(random_state=42, class_weight='balanced')
# Optional: Hyperparameter-Tuning ...
self.logger.info(f"Starte Training des Decision Tree Modells auf {X_train_imputed.shape[0]} Trainingsbeispielen mit {X_train_imputed.shape[1]} Features...")
start_fit_time = time.time()
dt_classifier.fit(X_train_imputed, y_train) # << MODELL WIRD HIER TRAINIERT
end_fit_time = time.time()
self.logger.info(f"Modelltraining abgeschlossen. Dauer: {end_fit_time - start_fit_time:.2f} Sekunden.")
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ HIER DEN CODE ZUM SPEICHERN DES MODELLS EINFÜGEN/VERSCHIEBEN ++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Speichern Sie das trainierte Modell.
self.model = dt_classifier # Zuweisung an self.model NACH dem Training
try:
model_dir = os.path.dirname(model_out)
if model_dir and not os.path.exists(model_dir): # Nur erstellen, wenn dirname nicht leer ist
if model_dir and not os.path.exists(model_dir):
os.makedirs(model_dir, exist_ok=True)
with open(model_out, 'wb') as f:
pickle.dump(dt_classifier, f)
pickle.dump(dt_classifier, f) # Das trainierte dt_classifier Objekt speichern
self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.")
except Exception as e:
self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}")
self.logger.debug(traceback.format_exc())
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ ENDE MODELLSPEICHERUNG ++++++++++++++++++++++++++++++++++++++++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Speichern Sie die Liste der Feature-Spalten
self._expected_features = feature_columns_ml # Stellen Sie sicher, dass feature_columns_ml hier korrekt ist
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ HIER DEN CODE ZUM SPEICHERN DER FEATURE-LISTE EINFÜGEN/VERSCHIEBEN +
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Speichern Sie die Liste der Feature-Spalten (fuer die Vorhersage)
# feature_columns_ml wurde bereits oben korrekt definiert
self._expected_features = feature_columns_ml
try:
patterns_data = {"feature_columns": feature_columns_ml, "target_classes": list(dt_classifier.classes_)}
patterns_data = {"feature_columns": self._expected_features, "target_classes": list(dt_classifier.classes_)}
patterns_dir = os.path.dirname(patterns_out)
if patterns_dir and not os.path.exists(patterns_dir): # Nur erstellen, wenn dirname nicht leer ist
if patterns_dir and not os.path.exists(patterns_dir):
os.makedirs(patterns_dir, exist_ok=True)
with open(patterns_out, 'w', encoding='utf-8') as f:
json.dump(patterns_data, f, indent=4, ensure_ascii=False)
json.dump(patterns_data, f, indent=4, ensure_ascii=False)
self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.")
except Exception as e:
self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}")
self.logger.debug(traceback.format_exc())
# Fahren Sie fort
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ ENDE FEATURE-LISTEN SPEICHERUNG +++++++++++++++++++++++++++++++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu bewerten)
self.logger.info("Starte Modellevaluation...") # <<< GEÄNDERT
# Vorhersagen auf dem Testset
self.logger.info("Starte Modellevaluation...")
y_pred = dt_classifier.predict(X_test_imputed)
# Metriken berechnen und loggen
accuracy = accuracy_score(y_test, y_pred)
self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}") # <<< GEÄNDERT
self.logger.info(f"Modell Genauigkeit auf dem Testset: {accuracy:.4f}")
class_report_labels = list(dt_classifier.classes_) # Sicherstellen, dass es eine Liste ist
class_report = classification_report(y_test, y_pred, zero_division=0, labels=class_report_labels, target_names=[str(c) for c in class_report_labels])
self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}")
# Klassifikationsbericht
# zero_division='warn' ist Standard, '0' gibt 0 fuer nicht vorhandene Klassen, 'none' wirft Fehler.
class_report = classification_report(y_test, y_pred, zero_division=0, labels=dt_classifier.classes_, target_names=[str(c) for c in dt_classifier.classes_]) # Stelle sicher, dass Labels und Target-Namen konsistent sind
self.logger.info(f"Klassifikationsbericht auf dem Testset:\n{class_report}") # <<< GEÄNDERT
cm = confusion_matrix(y_test, y_pred, labels=class_report_labels)
self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}")
# Konfusionsmatrix
# display_labels=dt_classifier.classes_ sorgt fuer korrekte Beschriftung
cm = confusion_matrix(y_test, y_pred, labels=dt_classifier.classes_)
self.logger.info(f"Konfusionsmatrix auf dem Testset (Zeilen=Wahr, Spalten=Vorhersage):\n{cm}") # <<< GEÄNDERT
# Entscheidungsregeln extrahieren (Optional, fuer Verstaendnis)
try:
# Beschraenken Sie die Tiefe fuer die Ausgabe, falls der Baum sehr tief ist
# feature_names muessen der Reihenfolge in X_train_imputed entsprechen
tree_rules = export_text(dt_classifier, feature_names=feature_columns, max_depth=7) # max_depth anpassen
self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}") # <<< GEÄNDERT
except Exception as e:
# Fange Fehler beim Exportieren der Regeln ab
self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}") # <<< GEÄNDERT
# Logge den Traceback.
self.logger.debug(traceback.format_exc()) # <<< GEÄNDERT
tree_rules = export_text(dt_classifier, feature_names=feature_columns_ml, max_depth=7)
self.logger.info(f"Erste Regeln des Decision Tree (max Tiefe 7):\n{tree_rules}")
except Exception as e_tree_rules: # Spezifischere Exception-Variable
self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e_tree_rules}")
self.logger.debug(traceback.format_exc())
self.logger.info("Modelltraining und -evaluation abgeschlossen.") # <<< GEÄNDERT
self.logger.info("Modelltraining und -evaluation abgeschlossen.")
# ==============================================================================
# Ende DataProcessor Klasse Utility: ML Prep & Training Block