bugfix
This commit is contained in:
@@ -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) ===============================
|
||||
# ==========================================================================
|
||||
|
||||
|
||||
Reference in New Issue
Block a user