From bf7f1d0be6baeee28a27c4c1c3f4250714b087c5 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 18 Jun 2025 09:34:59 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 124 ++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 70 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index d9b09465..26c78f2c 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -8618,91 +8618,74 @@ class DataProcessor: if self.model is None or self.imputer is None or self._expected_features is None: self.logger.info("Lade ML-Modell, Imputer und Feature-Spalten...") - try: - self._load_ml_model(MODEL_FILE, IMPUTER_FILE) - if self.model is None or self.imputer is None or self._expected_features is None: - self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen. Kann ML-Schaetzung nicht durchfuehren.") - return "FEHLER Schaetzung (Modell-Laden)" - self.logger.info("ML-Modell, Imputer und Feature-Spalten erfolgreich geladen.") - except Exception as e_load_model: - self.logger.error(f"FEHLER beim Laden von ML-Modell/Imputer/Feature-Spalten: {e_load_model}") - self.logger.debug(traceback.format_exc()) - return f"FEHLER Laden: {str(e_load_model)[:100]}..." + self._load_ml_model(MODEL_FILE, IMPUTER_FILE) + if self.model is None or self.imputer is None or self._expected_features is None: + self.logger.error("Laden von Modell, Imputer oder Feature-Spalten fehlgeschlagen.") + return "FEHLER Schaetzung (Modell-Laden)" try: - # Hole die konsolidierten Werte aus der row_data (Spalten BD und BE) - # Diese sollten durch vorherige Schritte (z.B. im Chat-Block von _process_single_row - # oder durch run_plausibility_checks_batch) bereits befüllt sein. + # === Feature Erstellung (muss exakt zum Training passen!) === + + # 1. Konsolidierte numerische Werte holen final_umsatz_val_str = self._get_cell_value_safe(row_data, "Finaler Umsatz (Wiki>CRM)") final_ma_val_str = self._get_cell_value_safe(row_data, "Finaler Mitarbeiter (Wiki>CRM)") - branche_val_str = self._get_cell_value_safe(row_data, "Chat Vorschlag Branche") # Oder CRM Branche, je nach Trainingsdaten - - # Numerische Werte für Vorhersage holen - # Wichtig: get_numeric_filter_value gibt 0 zurück, was wir als NaN für Imputation wollen + umsatz_for_pred = get_numeric_filter_value(final_umsatz_val_str, is_umsatz=True) ma_for_pred = get_numeric_filter_value(final_ma_val_str, is_umsatz=False) umsatz_for_pred = np.nan if umsatz_for_pred == 0 else umsatz_for_pred ma_for_pred = np.nan if ma_for_pred == 0 else ma_for_pred - - # Erstelle das 'is_part_of_group' Feature + + # 2. 'is_part_of_group' Feature erstellen parent_d_val = self._get_cell_value_safe(row_data, "Parent Account Name").strip().lower() parent_o_val = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip().lower() parent_p_val = self._get_cell_value_safe(row_data, "Parent Vorschlag Status").strip().lower() - cond1_pred = bool(parent_d_val and parent_d_val != 'k.a.') - cond2_o_pred = bool(parent_o_val and parent_o_val != 'k.a.') - cond2_p_pred = parent_p_val == 'x' - cond2_pred = cond2_o_pred and cond2_p_pred - is_group_val = 1 if cond1_pred | cond2_pred else 0 + cond2_pred = bool(parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x') + is_group_val = 1 if cond1_pred or cond2_pred else 0 + + # 3. Zusätzliche Features (Ratio, Log) erstellen + # Log-Transformationen + log_umsatz_val = np.log1p(umsatz_for_pred) if pd.notna(umsatz_for_pred) else np.nan + log_ma_val = np.log1p(ma_for_pred) if pd.notna(ma_for_pred) else np.nan - # Erstelle einen DataFrame für diese eine Zeile mit den Spaltennamen, - # die für das One-Hot-Encoding und die Angleichung an _expected_features benötigt werden. - # Die Namen hier MÜSSEN mit den internen Namen aus prepare_data_for_modeling übereinstimmen, - # BEVOR das One-Hot-Encoding dort stattfand (für Branche) und NACH der Konsolidierung. + # Umsatz pro MA + umsatz_pro_ma_val = np.nan + if pd.notna(umsatz_for_pred) and pd.notna(ma_for_pred) and ma_for_pred > 0: + umsatz_pro_ma_val = umsatz_for_pred / ma_for_pred + + # 4. Branchen-Feature holen + # Wichtig: Hier die gleiche Branchenspalte wie im Training verwenden! + branche_val_str = self._get_cell_value_safe(row_data, "CRM Branche") + + # DataFrame mit einer Zeile und den internen Namen (wie in prepare_data_for_modeling) erstellen single_row_dict = { - 'Finaler_Umsatz_ML': [umsatz_for_pred], # Konsolidierter Name aus prepare_data_for_modeling - 'Finaler_Mitarbeiter_ML': [ma_for_pred], # Konsolidierter Name aus prepare_data_for_modeling - 'branche_crm': [str(branche_val_str).strip() if branche_val_str else 'Unbekannt'], # Interner Name vor One-Hot - 'is_part_of_group': [is_group_val] + 'Log_Finaler_Umsatz_ML': [log_umsatz_val], + 'Log_Finaler_Mitarbeiter_ML': [log_ma_val], + 'Umsatz_pro_MA_ML': [umsatz_pro_ma_val], + 'is_part_of_group': [is_group_val], + 'branche_crm': [str(branche_val_str).strip() if branche_val_str else 'Unbekannt'] } df_single_row = pd.DataFrame.from_dict(single_row_dict) - # One-Hot Encoding (konsistent zum Training) - # Wichtig: 'branche_crm' muss der Name sein, der im Training one-hot-encodiert wurde. + # One-Hot Encoding df_encoded = pd.get_dummies(df_single_row, columns=['branche_crm'], prefix='Branche', dummy_na=False) - # Angleichung an die im Training verwendeten Features (self._expected_features) - df_processed = pd.DataFrame(columns=self._expected_features) + # Angleichung an die im Training verwendeten Features + # Erstelle einen DataFrame mit einer Zeile und den erwarteten Spalten + data_for_df_processed = {col: [0] for col in self._expected_features} for col in self._expected_features: - if col in df_encoded.columns: - df_processed[col] = df_encoded[col] - else: - # Wenn eine One-Hot-Spalte aus dem Training hier fehlt, wird sie mit 0 gefüllt - # Wenn 'Finaler_Umsatz_ML', 'Finaler_Mitarbeiter_ML' oder 'is_part_of_group' fehlen würde, - # wäre das ein Fehler in _expected_features oder der Logik hier. - df_processed[col] = 0 + if col in df_encoded.columns: + data_for_df_processed[col] = [df_encoded[col].iloc[0]] - # Numerische Features zu numerischem Typ konvertieren für Imputer - numeric_cols_to_impute_pred = ['Finaler_Umsatz_ML', 'Finaler_Mitarbeiter_ML'] - for col in numeric_cols_to_impute_pred: - if col in df_processed.columns: - df_processed[col] = pd.to_numeric(df_processed[col], errors='coerce') - else: # Sollte nicht passieren, wenn _expected_features korrekt ist - self.logger.warning(f"Erwartetes numerisches Feature '{col}' nicht in df_processed für Vorhersage gefunden.") - df_processed[col] = np.nan + df_processed = pd.DataFrame(data_for_df_processed, columns=self._expected_features) + + # Imputation und Vorhersage + df_imputed_array = self.imputer.transform(df_processed) + + prediction_proba = self.model.predict_proba(df_imputed_array) + predicted_bucket_label = self.model.classes_[np.argmax(prediction_proba[0])] - - # Imputation - df_imputed_array = self.imputer.transform(df_processed[self._expected_features]) # Stelle sicher, dass Spaltenreihenfolge passt - df_imputed = pd.DataFrame(df_imputed_array, columns=self._expected_features) - - # Vorhersage - prediction_proba = self.model.predict_proba(df_imputed) - model_classes = self.model.classes_ - predicted_class_index = np.argmax(prediction_proba[0]) - predicted_bucket_label = model_classes[predicted_class_index] - - self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}' (Wahrscheinlichkeiten: {prediction_proba[0]})") + self.logger.debug(f" -> ML Vorhersage Ergebnis: '{predicted_bucket_label}'") return predicted_bucket_label except Exception as e_predict: @@ -8712,6 +8695,7 @@ class DataProcessor: + # --- Methode zum Laden des ML Modells und Imputers --- # Diese Methode wird von _predict_technician_bucket (denselben Block) und train_technician_model (denselben Block) aufgerufen. # Sie laedt die serialisierten Modelle von der Festplatte. @@ -9038,7 +9022,7 @@ class DataProcessor: -# --- Features konsolidieren (Umsatz, Mitarbeiter) --- + # --- Features konsolidieren (Umsatz, Mitarbeiter) --- self.logger.debug("Konsolidiere Umsatz und Mitarbeiter für ML-Features...") cols_to_process_ml = { 'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz_ML'), @@ -9166,12 +9150,12 @@ class DataProcessor: return df_model_ready - # 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). + # 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): self.logger.info("Starte Training des Servicetechniker Decision Tree Modells...")