diff --git a/brancheneinstufung.py b/brancheneinstufung.py index d0481fea..526772a9 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -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