From c4484f5e599dae64c008aa3557090fe31ba9f462 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 7 May 2025 08:42:28 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 494 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 492 insertions(+), 2 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 313f40b2..44ca4f89 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -12125,9 +12125,499 @@ if __name__ == '__main__': # Die erste Zeile sollte die Spaltennamen enthalten. headers = all_data[0] # Stellen Sie sicher, dass die Header-Zeile auch die erwartete Mindestlaenge hat, - # um die Spa + # um die Spaltenindizes aus COLUMN_MAP (Block 1) zu finden. + try: + max_col_idx_in_map = max(COLUMN_MAP.values()) # Finde den hoechsten Index in COLUMN_MAP + # Pruefen Sie, ob die Anzahl der geladenen Spalten im Header ausreicht + if len(headers) <= max_col_idx_in_map: + # Logge einen kritischen Fehler, wenn das Mapping auf Spalten zeigt, die nicht im Sheet existieren + self.logger.critical(f"FEHLER: Header-Zeile ({len(headers)} Spalten) ist kuerzer als der hoechste Index in COLUMN_MAP ({max_col_idx_in_map}). COLUMN_MAP passt nicht zum Sheet.") + return None # Beende die Methode + except ValueError: # Tritt auf, wenn COLUMN_MAP leer ist + self.logger.critical("FEHLER: COLUMN_MAP scheint leer zu sein oder enthaelt keine Werte. Kann Max Index nicht ermitteln.") + return None # Beende die Methode + except Exception as e: + # Fange andere unerwartete Fehler ab + self.logger.critical(f"FEHLER beim Pruefen der Spaltenlaenge der Header-Zeile: {e}") + return None # Beende die Methode + + except IndexError: + # Wenn das Sheet leer ist oder keine erste Zeile hat + self.logger.critical("FEHLER: Sheet scheint leer zu sein oder hat keine erste Zeile, keine Header gefunden.") + return None # Beende die Methode + except Exception as e: + # Fange andere unerwartete Fehler beim Zugriff auf Header ab + self.logger.critical(f"FEHLER beim Zugriff auf Header: {e}") + # Logge den Traceback + self.logger.debug(traceback.format_exc()) + return None # Beende die Methode + + + # Datenzeilen sind alle Zeilen nach den Header-Zeilen + data_rows = all_data[header_rows:] # Annahme: Die ersten X Zeilen sind Header + + # Erstelle DataFrame aus den Datenzeilen und den Headern + df = pd.DataFrame(data_rows, columns=headers) + self.logger.info(f"Initialen DataFrame fuer Modellierung erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.") + + # --- Spaltenauswahl und Umbenennung --- + # Definiere die notwendigen Spalten anhand ihrer COLUMN_MAP Schluessel (Block 1) + # und weisen ihnen interne, einfachere Namen zu, die im DataFrame verwendet werden. + col_keys_mapping = { + "name": "CRM Name", # Zur Identifikation, wird spaeter entfernt + "branche_crm": "CRM Branche", # Fuer One-Hot Encoding + "umsatz_crm": "CRM Umsatz", # Fuer Konsolidierung + "umsatz_wiki": "Wiki Umsatz", # Fuer Konsolidierung + "ma_crm": "CRM Anzahl Mitarbeiter", # Fuer Konsolidierung + "ma_wiki": "Wiki Mitarbeiter", # Fuer Konsolidierung + "techniker": "CRM Anzahl Techniker" # DIE ZIELVARIABLE (Bekannte Technikerzahl) + } + + # Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP (Block 1) vorhanden sind + missing_keys_in_map = [key for key in col_keys_mapping.values() if key not in COLUMN_MAP] + if missing_keys_in_map: + self.logger.critical(f"FEHLER: Folgende benoetigte Spalten-Schluessel fehlen in COLUMN_MAP fuer prepare_data_for_modeling: {missing_keys_in_map}.") + return None # Beende die Methode + + # Erstelle das Mapping von tatsaechlichen Header-Namen zu internen Schluesseln. + # Verwende die Header-Namen aus dem geladenen Sheet und die COLUMN_MAP, um die richtigen Header zu finden. + header_to_internal_key = {} # Dict zum Umbenennen der Spalten + cols_to_select_by_header = [] # Liste der Header-Namen, die aus dem DF ausgewaehlt werden + + try: + # Iteriere ueber das Mapping von internen zu COLUMN_MAP Schluesseln + for internal_key, column_map_key in col_keys_mapping.items(): + # Hole den tatsaechlichen Header-Namen aus dem Sheet + header_name_from_sheet = headers[COLUMN_MAP[column_map_key]] + # Fuege das Mapping hinzu + header_to_internal_key[header_name_from_sheet] = internal_key + # Fuege den Header-Namen zur Liste der auszuwaehlenden Spalten hinzu + cols_to_select_by_header.append(header_name_from_sheet) + + # Waehle nur die benoetigten Spalten im DataFrame aus + df_subset = df[cols_to_select_by_header].copy() # Kopie erstellen, um SettingWithCopyWarning zu vermeiden + # Benenne die Spalten um zu den internen Namen + df_subset.rename(columns=header_to_internal_key, inplace=True) + + except KeyError as e: + # Dieser Fehler sollte eigentlich durch die obige Pruefung abgefangen werden, + # tritt aber auf, wenn ein erwarteter Header-Name nicht im geladenen DF ist (selten, wenn COLUMN_MAP korrekt ist). + self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (KeyError: '{e}'). Der Header wurde nicht im DataFrame gefunden.") + self.logger.debug(f"Erwartete Header: {cols_to_select_by_header}. Verfuegbare Header im DF: {list(df.columns)}") + return None # Beende die Methode + except IndexError as e: + # Tritt auf, wenn COLUMN_MAP einen Index > Anzahl Spalten im DF hat + self.logger.critical(f"FEHLER beim Auswaehlen/Umbenennen der Spalten (IndexError: '{e}'). COLUMN_MAP zeigt auf Spalten, die nicht im geladenen Sheet existieren.") + self.logger.debug(f"COLUMN_MAP: {COLUMN_MAP}. Sheet hat {len(headers)} Spalten.") + return None # Beende die Methode + except Exception as e: + # Fange andere unerwartete Fehler ab + self.logger.critical(f"Unerwarteter FEHLER beim Auswaehlen/Umbenennen der Spalten: {e}") + # Logge den Traceback + self.logger.debug(traceback.format_exc()) + return None # Beende die Methode + + + self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") + + # --- Features konsolidieren (Umsatz, Mitarbeiter) --- + # Nutzt die globale Hilfsfunktion get_valid_numeric (Block 5), die numerische Werte als Float/Int oder NaN zurueckgibt. + cols_to_process = { + 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'), + 'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter') + } + + for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): + self.logger.debug(f"Verarbeite und konsolidiere '{base_name}' (Prioritaet: Wiki > CRM)...") + # Sicherstellen, dass die Spalten im df_subset existieren, bevor apply aufgerufen wird. + # Dies sollte durch die Spaltenauswahl oben garantiert sein, aber zur Sicherheit. + wiki_series = df_subset[wiki_col].apply(get_valid_numeric) if wiki_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) + crm_series = df_subset[crm_col].apply(get_valid_numeric) if crm_col in df_subset.columns else pd.Series(np.nan, index=df_subset.index) + + + # np.where waehlt den Wiki-Wert, wenn er nicht NaN ist, sonst den CRM-Wert. + df_subset[final_col] = np.where( + wiki_series.notna(), # Wenn Wiki-Wert vorhanden ist (nicht NaN) + wiki_series, # Nimm den Wiki-Wert + crm_series # Sonst nimm den CRM-Wert (der auch NaN sein kann) + ) + # Info-Log ueber Ergebnis + self.logger.info(f" -> {df_subset[final_col].notna().sum()} gueltige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).") + + # --- Zielvariable vorbereiten (Technikerzahl) --- + techniker_col_internal = "techniker" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping) + self.logger.info(f"Verarbeite Zielvariable '{techniker_col_internal}'...") + + # Sicherstellen, dass die Spalte existiert + if techniker_col_internal not in df_subset.columns: + self.logger.critical(f"FEHLER: Zielvariable '{techniker_col_internal}' (CRM Anzahl Techniker) nicht im DataFrame gefunden nach Umbenennung.") + return None # Beende die Methode + + # Konvertiere zu Numerisch (Float/Int oder NaN) mit get_valid_numeric (Block 5). + # Dies stellt sicher, dass nur gueltige, positive Zahlen verwendet werden. + df_subset['Anzahl_Servicetechniker_Numeric'] = df_subset[techniker_col_internal].apply(get_valid_numeric) + + + # Filtere Zeilen: Behalte nur die mit gueltiger, positiver Technikerzahl (Float > 0). + initial_rows = len(df_subset) + # Hier filtern wir basierend auf der numerischen Spalte, die durch get_valid_numeric erstellt wurde. + df_filtered = df_subset[ + df_subset['Anzahl_Servicetechniker_Numeric'].notna() & # Nicht NaN + (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) # Und groesser als 0 + ].copy() # WICHTIG: .copy() um SettingWithCopyWarning zu vermeiden + filtered_rows = len(df_filtered) + removed_rows = initial_rows - filtered_rows + + # Info, wenn Zeilen entfernt wurden + if removed_rows > 0: + self.logger.info(f"{removed_rows} Zeilen entfernt aufgrund fehlender/ungueltiger Technikerzahl (Wert <= 0 oder nicht numerisch/parsebar).") + self.logger.info(f"Verbleibende Zeilen fuer Modellierungstraining (mit gueltiger Technikerzahl > 0): {filtered_rows}") + + # Wenn keine Zeilen uebrig bleiben, kann kein Modell trainiert werden. + if filtered_rows == 0: + self.logger.error("FEHLER: Keine Zeilen mit gueltiger Technikerzahl (>0) uebrig fuer Modellierungstraining!") + return None # Beende die Methode + + + # --- Techniker-Buckets erstellen --- + # Die Bins und Labels muessen die gefilterten Daten widerspiegeln (die jetzt alle > 0 sind). + # Die Bin-Definition muss so sein, dass alle Werte > 0 einem Bucket zugeordnet werden. + # Beispiel: (-1, 0] -> Bucket 1 (0), (0, 19] -> Bucket 2 (<20), (19, 49] -> Bucket 3 (<50) etc. + # Da wir auf >0 filtern, landet 0 nie im Trainingsset, aber die Bin-Definition muss trotzdem Sinn ergeben. + bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] # Definiere die Grenzen der Buckets + labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)'] # Namen fuer die Buckets + + try: + # Erstellen Sie die Bucket-Spalte mit pd.cut + df_filtered['Techniker_Bucket'] = pd.cut( + df_filtered['Anzahl_Servicetechniker_Numeric'], # Die numerische Technikerzahl-Spalte + bins=bins, # Die definierten Grenzen + labels=labels, # Die definierten Labels + right=True, # Intervalle sind (links, rechts]. z.B. (0, 19] inkludiert 19. + include_lowest=True # Inkludiert den niedrigsten Wert der ersten Bin (-1) (relevant, falls 0 moeglich waere) + ) + self.logger.info("Techniker-Buckets erstellt.") + + # Pruefe, ob NaNs in Buckets erstellt wurden (sollte bei >0 Filterung und korrekten Bins nicht passieren). + if df_filtered['Techniker_Bucket'].isna().any(): + nan_bucket_rows = df_filtered['Techniker_Bucket'].isna().sum() + self.logger.warning(f"WARNUNG: {nan_bucket_rows} Zeilen mit NaNs in Techniker-Buckets nach pd.cut erstellt. Ueberpruefen Sie die bins/labels oder die Filterung.") + # Entfernen Sie diese Zeilen, da sie nicht zum Trainieren verwendet werden koennen. + df_filtered.dropna(subset=['Techniker_Bucket'], inplace=True) # Entferne Zeilen mit NaN im Bucket + self.logger.info(f"Nach Entfernung von {nan_bucket_rows} Zeilen mit NaN Buckets: {len(df_filtered)} Zeilen verbleiben fuer Training.") + # Wenn nach Entfernung keine Zeilen mehr uebrig sind + if len(df_filtered) == 0: + self.logger.error("FEHLER: Keine Zeilen uebrig nach Entfernung von NaN Buckets. Modell kann nicht trainiert werden.") + return None # Beende die Methode + + + # Verteilung der Buckets als Info-Log (absolute Haeufigkeit und Prozent) + self.logger.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz ({len(df_filtered)} Zeilen):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=False).sort_index()}") # Zaehlung + self.logger.info(f"Verteilung (Prozent):\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).sort_index().round(3)}") # Prozent + + except Exception as e: + # Fange Fehler beim Erstellen der Buckets ab + self.logger.critical(f"FEHLER beim Erstellen der Techniker-Buckets: {e}") + # Logge den Traceback + self.logger.debug(traceback.format_exc()) + return None # Beende die Methode + + + # --- Kategoriale Features vorbereiten (Branche) --- + branche_col_internal = "branche_crm" # Interne Spaltenname nach Umbenennung (aus col_keys_mapping) + self.logger.info(f"Verarbeite kategoriales Feature '{branche_col_internal}' fuer One-Hot Encoding...") + + # Sicherstellen, dass die Spalte existiert + if branche_col_internal not in df_filtered.columns: + self.logger.critical(f"FEHLER: Spalte '{branche_col_internal}' nicht im DataFrame fuer One-Hot Encoding gefunden.") + return None # Beende die Methode + + # Stelle sicher, dass die Spalte String ist und fülle evtl. NaNs mit 'Unbekannt'. + # .str.strip() entfernt führende/endende Leerzeichen. + df_filtered[branche_col_internal] = df_filtered[branche_col_internal].astype(str).fillna('Unbekannt').str.strip() + + + # One-Hot Encoding (pd.get_dummies) + # dummy_na=False, da wir NaNs bereits mit 'Unbekannt' gefuellt haben. + # prefix='Branche' ist gut, um die neuen Spalten zu identifizieren. + df_encoded = pd.get_dummies(df_filtered, columns=[branche_col_internal], prefix='Branche', dummy_na=False) + self.logger.info(f"One-Hot Encoding fuer '{branche_col_internal}' durchgefuehrt. Neue Spaltenanzahl: {len(df_encoded.columns)}") + + + # --- Finale Auswahl der Features fuer das Modell --- + # Identifizieren Sie die Feature-Spalten nach dem Encoding. + # Dies sind alle Spalten, die mit 'Branche_' beginnen (One-Hot), plus die konsolidierten numerischen. + feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')] # Alle One-Hot Branch-Spalten + # Fuegen Sie die konsolidierten numerischen Spalten hinzu. + feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) + + + # Pruefen Sie, ob die konsolidierten numerischen Spalten ('Finaler_Umsatz', 'Finaler_Mitarbeiter') + # tatsaechlich im DataFrame df_encoded vorhanden sind (sollten sie, wurden oben erstellt). + if not all(col in df_encoded.columns for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']): + self.logger.critical("FEHLER: Konsolidierte numerische Spalten 'Finaler_Umsatz' oder 'Finaler_Mitarbeiter' fehlen im DataFrame nach Konsolidierung.") + return None # Beende die Methode + + + target_column = 'Techniker_Bucket' # Die Zielvariable + + + # Erstellen Sie den finalen DataFrame nur mit Features und Target (und Identifikationsspalten). + # Behalten Sie 'name' und 'Anzahl_Servicetechniker_Numeric' fuer Reporting/Debugging. + # 'name' ist der interne Name nach Umbenennung (aus col_keys_mapping). + identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] + # Sicherstellen, dass diese Identifikationsspalten auch im DataFrame existieren. + if not all(col in df_encoded.columns for col in identification_cols): + self.logger.critical(f"FEHLER: Identifikationsspalten {identification_cols} fehlen im DataFrame.") + return None # Beende die Methode + + + # Erstellen Sie die Liste der finalen Spalten fuer den DataFrame + # Stellen Sie sicher, dass alle Feature-Spalten und die Zielspalte auch wirklich im DataFrame sind + # (Koennte fehlen, wenn z.B. Finaler_Umsatz/Mitarbeiter oben fehlschlug und als NaN resultierte, was aber ok ist fuer Imputer). + final_cols_for_df = identification_cols + feature_columns + [target_column] + missing_final_cols = [col for col in final_cols_for_df if col not in df_encoded.columns] + if missing_final_cols: + self.logger.critical(f"FEHLER: Finale Spalten fuer Modellierung fehlen im DataFrame: {missing_final_cols}") + return None # Beende die Methode + + + # Erstellen Sie den finalen DataFrame mit den ausgewaehlten Spalten. + df_model_ready = df_encoded[final_cols_for_df].copy() # Erstelle eine Kopie + + + # Optional: Konvertieren Sie numerische Spalten explizit zu Float64. + # Dies stellt sicher, dass der Imputer korrekt arbeitet. + for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter', 'Anzahl_Servicetechniker_Numeric']: + if col in df_model_ready.columns: # Sicherheitscheck, ob Spalte existiert + # errors='coerce' wandelt Fehler bei der Konvertierung in NaN. Wichtig, da Imputer NaNs erwartet. + df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') + + + # Setzen Sie den Index des DataFrames zurueck, um eine saubere Verarbeitung in den naechsten Schritten + # (z.B. Train/Test-Split) sicherzustellen. drop=True verhindert, dass der alte Index als neue Spalte hinzugefuegt wird. + df_model_ready = df_model_ready.reset_index(drop=True) + + + # Logge Informationen zum finalen DataFrame + self.logger.info("Datenvorbereitung fuer Modellierung (Training) abgeschlossen.") + self.logger.info(f"Finaler DataFrame fuer Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") + # Logge die Anzahl der Feature-Spalten, nicht die Liste selbst (kann sehr lang sein). + self.logger.info(f"Anzahl Feature-Spalten: {len(feature_columns)}") + self.logger.info(f"Ziel-Spalte: {target_column}") + + + # WICHTIG: Info ueber fehlende Werte in den finalen numerischen Features VOR der Imputation. + # Die Imputation selbst erfolgt im Trainingsschritt (train_technician_model Block 31). + numeric_features_for_imputation = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] + nan_counts = df_model_ready[numeric_features_for_imputation].isna().sum() + self.logger.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") + # Logge auch, wie viele Zeilen *mindestens* einen NaN in den numerischen Features haben. + rows_with_nan = df_model_ready[numeric_features_for_imputation].isna().any(axis=1).sum() + self.logger.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature (vor Imputation): {rows_with_nan}") + + + return df_model_ready # Gebe den vorbereiteten DataFrame zurueck + + + # Methode zum Trainieren des ML Modells + # Nutzt interne Methode: prepare_data_for_modeling. + # Nutzt globale Helfer: MODEL_FILE, IMPUTER_FILE, PATTERNS_FILE_JSON (Block 1), + # logger, pickle, json, os, + # 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. + + 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...") + + # 1. Daten vorbereiten (nutzt die interne Methode prepare_data_for_modeling denselben Block) + 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.") + return # Beende die Methode + + + # Separate Features (X) und Target (y) + # Identifikationsspalten und Zielspalte (muss konsistent mit prepare_data_for_modeling sein) + identification_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] + target_column = 'Techniker_Bucket' + + + # 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.") + return # Beende die Methode + + # Erstellen Sie die Feature-Matrix X und den Zielvektor y + X = df_model_ready[feature_columns] + y = df_model_ready[target_column] + + + self.logger.info(f"Daten fuer Training vorbereitet. X Shape: {X.shape}, y Shape: {y.shape}") + # 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]}...") + + + # 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.") + + # 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...") + # Fitten Sie den Imputer NUR auf den Trainingsdaten, um Data Leakage zu vermeiden. + imputer.fit(X_train) # Fitten Sie den Imputer auf X_train + + + # Speichern Sie den Imputer (wird fuer Vorhersagen benoetigt). + self.imputer = imputer # Speichern Sie ihn in der Instanz fuer spaetere Vorhersagen + try: + # Stellen Sie sicher, dass das Verzeichnis fuer den Output existiert. + os.makedirs(os.path.dirname(imputer_out), exist_ok=True) # Erstellt das Verzeichnis, falls es nicht existiert + # Speichern Sie den Imputer mit pickle + with open(imputer_out, 'wb') as f: + pickle.dump(imputer, f) + self.logger.info(f"Imputer erfolgreich gespeichert in '{imputer_out}'.") + except Exception as e: + # Fange Fehler beim Speichern ab und logge sie. + self.logger.error(f"FEHLER beim Speichern des Imputers in '{imputer_out}': {e}") + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) + # Fahren Sie fort, aber loggen Sie den Fehler + + + # Transformieren Sie Trainings- und Testdaten mit dem gefitteten Imputer. + X_train_imputed = imputer.transform(X_train) + X_test_imputed = imputer.transform(X_test) + + # Konvertieren Sie die Ergebnisse (Numpy Arrays) zurueck zu DataFrames, behalten Sie die Spaltennamen. + X_train_imputed = pd.DataFrame(X_train_imputed, columns=feature_columns) + X_test_imputed = pd.DataFrame(X_test_imputed, columns=feature_columns) + self.logger.info("Numerische Features imputiert.") + + + # 4. Decision Tree Training + # Definieren Sie das Decision Tree Modell. + # Wichtige Hyperparameter zum Tunen: max_depth, min_samples_split, min_samples_leaf. + # class_weight='balanced' ist hilfreich bei ungleicher Klassenverteilung (wahrscheinlich bei Buckets). + dt_classifier = DecisionTreeClassifier(random_state=42, class_weight='balanced') + # Optional: Hyperparameter-Tuning mit GridSearchCV + # param_grid = {'max_depth': [None, 10, 20, 30], 'min_samples_split': [2, 5, 10], 'min_samples_leaf': [1, 2, 5]} + # grid_search = GridSearchCV(dt_classifier, param_grid, cv=5, scoring='accuracy') + # grid_search.fit(X_train_imputed, y_train) + # dt_classifier = grid_search.best_estimator_ + # self.logger.info(f"Beste Parameter gefunden durch GridSearchCV: {grid_search.best_params_}") + + + self.logger.info("Starte Training des Decision Tree Modells...") + # Fitten Sie das Modell auf den imputierten Trainingsdaten. + dt_classifier.fit(X_train_imputed, y_train) + self.logger.info("Modelltraining abgeschlossen.") + + + # Speichern Sie das trainierte Modell. + self.model = dt_classifier # Speichern Sie es in der Instanz fuer spaetere Vorhersagen + try: + # Stellen Sie sicher, dass das Verzeichnis fuer den Output existiert. + os.makedirs(os.path.dirname(model_out), exist_ok=True) # Erstellt das Verzeichnis, falls es nicht existiert + # Speichern Sie das Modell mit pickle + with open(model_out, 'wb') as f: + pickle.dump(dt_classifier, f) + self.logger.info(f"Decision Tree Modell erfolgreich gespeichert in '{model_out}'.") + except Exception as e: + # Fange Fehler beim Speichern ab und logge sie. + self.logger.error(f"FEHLER beim Speichern des Modells in '{model_out}': {e}") + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) + # Fahren Sie fort + + + # Speichern Sie die Liste der Feature-Spalten (fuer die Vorhersage) + self._expected_features = feature_columns # Speichern Sie diese Liste in der Instanz fuer _predict_technician_bucket + try: + # Speichern als JSON fuer bessere Lesbarkeit und um zusaetzliche Infos (wie Klassen) zu speichern. + # PATTERNS_FILE_JSON wird aus Config (Block 1) geholt. + patterns_data = {"feature_columns": feature_columns, "target_classes": list(dt_classifier.classes_)} + # Stellen Sie sicher, dass das Verzeichnis fuer den Output existiert. + os.makedirs(os.path.dirname(patterns_out), exist_ok=True) # Erstellt das Verzeichnis, falls es nicht existiert + # Speichern Sie die JSON-Datei + with open(patterns_out, 'w', encoding='utf-8') as f: + json.dump(patterns_data, f, indent=4, ensure_ascii=False) + self.logger.info(f"Erwartete Feature-Spalten und Klassen erfolgreich gespeichert in '{patterns_out}'.") + + # Optional: Speichern als einfache Textdatei (wie im Originalcode) + # patterns_out_txt = patterns_out.replace('.json', '.txt') + # with open(patterns_out_txt, 'w', encoding='utf-8') as f: + # for col in feature_columns: f.write(f"{col}\n") + # self.logger.info(f"Erwartete Feature-Spalten (txt) erfolgreich gespeichert in '{patterns_out_txt}'.") + + except Exception as e: + # Fange Fehler beim Speichern ab und logge sie. + self.logger.error(f"FEHLER beim Speichern der Feature-Spalten in '{patterns_out}': {e}") + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) + # Fahren Sie fort + + + # 5. Evaluation (Optional, aber empfohlen, um die Modellleistung zu bewerten) + self.logger.info("Starte Modellevaluation...") + + # Vorhersagen auf dem Testset + 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}") + + # 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}") + + # 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}") + + # 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}") + except Exception as e: + # Fange Fehler beim Exportieren der Regeln ab + self.logger.warning(f"FEHLER beim Exportieren der Baumregeln: {e}") + # Logge den Traceback. + self.logger.debug(traceback.format_exc()) + + self.logger.info("Modelltraining und -evaluation abgeschlossen.") + +# ============================================================================== +# Ende DataProcessor Klasse Utility: ML Prep & Training Block +# ============================================================================== - # ========================================================================== + # ========================================================================== # === Utility Methods (Other Specific Tasks) =============================== # ==========================================================================