diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 4a9d71ce..d1968c16 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -5563,58 +5563,46 @@ class DataProcessor: return np.nan raw_value_str = str(value_str) try: - # Kopieren Sie hier die Logik von extract_numeric_value, die NaN zurückgibt - # anstatt "k.A." bei Fehlern oder 0/negativen Werten. - processed_value = clean_text(raw_value_str) # Annahme: clean_text existiert - if processed_value == "k.A.": - return np.nan + # Kopieren Sie hier die Logik von extract_numeric_value, die NaN zurückgibt + # anstatt "k.A." bei Fehlern oder 0/negativen Werten. + processed_value = clean_text(raw_value_str) # Annahme: clean_text existiert + if processed_value == "k.A.": + return np.nan - processed_value = re.sub( - r'(?i)^\s*(ca\.?|circa|rund|etwa|über|unter|mehr als|weniger als|bis zu)\s+', - '', - processed_value - ) - processed_value = re.sub(r'[€$£¥]', '', processed_value).strip() - processed_value = re.split( - r'\s*(-|–|bis)\s*', - processed_value, - 1 - )[0].strip() - processed_value_no_thousands = processed_value.replace('.', '').replace("'", "") - processed_value_final = processed_value_no_thousands.replace(',', '.') + processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|über|unter|mehr als|weniger als|bis zu)\s+', '', processed_value) + processed_value = re.sub(r'[€$£¥]', '', processed_value).strip() + processed_value = re.split(r'\s*(-|–|bis)\s*', processed_value, 1)[0].strip() + processed_value_no_thousands = processed_value.replace('.', '').replace("'", "") + processed_value_final = processed_value_no_thousands.replace(',', '.') - match = re.search(r'([\d.]+)', processed_value_final) - if not match: - return np.nan + match = re.search(r'([\d.]+)', processed_value_final) + if not match: + return np.nan - num_str = match.group(1) - if not num_str or num_str == '.': - return np.nan + num_str = match.group(1) + if not num_str or num_str == '.': + return np.nan - num = float(num_str) + num = float(num_str) - original_lower = raw_value_str.lower() - multiplier = 1.0 - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - multiplier = 1000000000.0 - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill\.\s*\b', original_lower): - multiplier = 1000000.0 - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - multiplier = 1000.0 + original_lower = raw_value_str.lower() + multiplier = 1.0 + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + multiplier = 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill\.\s*\b', original_lower): + multiplier = 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + multiplier = 1000.0 - num = num * multiplier - return num if num > 0 else np.nan # Nur positive Werte zählen + num = num * multiplier + + return num if num > 0 else np.nan # Nur positive Werte zählen except (ValueError, TypeError) as e: - logging.debug( - f"Konntze Wert '{str(value_str)[:50]}...' nicht als gültige Zahl parsen: {e}" - ) + logging.debug(f"Konntze Wert '{str(value_str)[:50]}...' nicht als gültige Zahl parsen: {e}") return np.nan except Exception as e: - logging.warning( - f"Unerwarteter Fehler in get_valid_numeric für Wert " - f"'{str(value_str)[:50]}...': {e}" - ) + logging.warning(f"Unerwarteter Fehler in get_valid_numeric für Wert '{str(value_str)[:50]}...': {e}") return np.nan @@ -5624,20 +5612,10 @@ class DataProcessor: } for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items(): - logging.info( - f"Verarbeite und konsolidiere '{base_name}' (Priorität: Wiki > CRM)..." - ) - + logging.info(f"Verarbeite und konsolidiere '{base_name}' (Priorität: Wiki > CRM)...") # Sicherstellen, dass Spalten existieren (get_valid_numeric behandelt None) - if wiki_col in df_subset.columns: - wiki_series = df_subset[wiki_col].apply(get_valid_numeric) - else: - wiki_series = pd.Series(np.nan, index=df_subset.index) - - if crm_col in df_subset.columns: - crm_series = df_subset[crm_col].apply(get_valid_numeric) - else: - crm_series = pd.Series(np.nan, index=df_subset.index) + 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 wählt den ersten Wert, wenn er nicht NaN ist, sonst den zweiten df_subset[final_col] = np.where( @@ -5645,16 +5623,14 @@ class DataProcessor: wiki_series, crm_series ) - - logging.info( - f" -> {df_subset[final_col].notna().sum()} gültige " - f"'{final_col}' Werte erstellt (von {len(df_subset)} Zeilen)." - ) + logging.info(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt (von {len(df_subset)} Zeilen).") # --- Zielvariable vorbereiten (Technikerzahl) --- - techniker_col = "techniker" # Interne Spaltenname nach Umbenennung + techniker_col = "techniker" # Interne Spaltenname nach Umbenennung logging.info(f"Verarbeite Zielvariable '{techniker_col}'...") - if techniker_col not in df_subset.columns: logging.critical(f"FEHLER: Zielvariable '{techniker_col}' fehlt."); return None + if techniker_col not in df_subset.columns: + logging.critical(f"FEHLER: Zielvariable '{techniker_col}' fehlt.") + return None df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce') # Filtere Zeilen: Behalte nur die mit gültiger, positiver Technikerzahl (> 0) @@ -5663,25 +5639,46 @@ class DataProcessor: df_subset['Anzahl_Servicetechniker_Numeric'].notna() & (df_subset['Anzahl_Servicetechniker_Numeric'] > 0) ].copy() - filtered_rows = len(df_filtered); removed_rows = initial_rows - filtered_rows; - if removed_rows > 0: logging.info(f"{removed_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).") + filtered_rows = len(df_filtered) + removed_rows = initial_rows - filtered_rows + if removed_rows > 0: + logging.info(f"{removed_rows} Zeilen entfernt (fehlende/ungültige Technikerzahl).") logging.info(f"Verbleibende Zeilen für Modellierung (mit gültiger Technikerzahl > 0): {filtered_rows}") - if filtered_rows == 0: logging.error("FEHLER: Keine Zeilen mit gültiger Technikerzahl (>0) übrig!"); return None + if filtered_rows == 0: + logging.error("FEHLER: Keine Zeilen mit gültiger Technikerzahl (>0) übrig!") + return None # --- Techniker-Buckets erstellen --- - # Bins und Labels wie definiert ([-1, 0, 19, 49, 99, 249, 499, float('inf')]) bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] - labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)'] - df_filtered['Techniker_Bucket'] = pd.cut( df_filtered['Anzahl_Servicetechniker_Numeric'], bins=bins, labels=labels, right=True, include_lowest=True ) + labels = [ + 'Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', + 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)' + ] + df_filtered['Techniker_Bucket'] = pd.cut( + df_filtered['Anzahl_Servicetechniker_Numeric'], + bins=bins, + labels=labels, + right=True, + include_lowest=True + ) logging.info("Techniker-Buckets erstellt.") logging.info(f"Verteilung der Techniker-Buckets im Trainingsdatensatz:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") - if df_filtered['Techniker_Bucket'].isna().any(): logging.warning("WARNUNG: NaNs in Techniker-Buckets erstellt. Entferne diese Zeilen."); df_filtered.dropna(subset=['Techniker_Bucket'], inplace=True); logging.info(f"Nach Entfernung von NaN Buckets: {len(df_filtered)} Zeilen verbleiben."); if len(df_filtered) == 0: logging.error("FEHLER: Keine Zeilen übrig nach Entfernung von NaN Buckets."); return None; + # NaN-Buckets entfernen + if df_filtered['Techniker_Bucket'].isna().any(): + logging.warning("WARNUNG: NaNs in Techniker-Buckets erstellt. Entferne diese Zeilen.") + df_filtered.dropna(subset=['Techniker_Bucket'], inplace=True) + logging.info(f"Nach Entfernung von NaN Buckets: {len(df_filtered)} Zeilen verbleiben.") + if len(df_filtered) == 0: + logging.error("FEHLER: Keine Zeilen übrig nach Entfernung von NaN Buckets.") + return None # --- Kategoriale Features vorbereiten (Branche) --- - branche_col = "branche" # Interne Spaltenname + branche_col = "branche" # Interne Spaltenname logging.info(f"Verarbeite kategoriales Feature '{branche_col}' für One-Hot Encoding...") - if branche_col not in df_filtered.columns: logging.critical(f"FEHLER: Spalte '{branche_col}' fehlt für One-Hot Encoding."); return None; + if branche_col not in df_filtered.columns: + logging.critical(f"FEHLER: Spalte '{branche_col}' fehlt für One-Hot Encoding.") + return None df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False) logging.info(f"One-Hot Encoding für '{branche_col}' durchgeführt. Neue Spaltenanzahl: {len(df_encoded.columns)}") @@ -5689,19 +5686,29 @@ class DataProcessor: # --- Finale Auswahl der Features für das Modell --- feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')] feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter']) - if not all(col in df_encoded.columns for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']): logging.critical("FEHLER: Konsolidierte numerische Spalten fehlen."); return None; - target_column = 'Techniker_Bucket'; original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric']; - if not all(col in df_encoded.columns for col in original_data_cols): logging.critical(f"FEHLER: Originaldaten-Spalten {original_data_cols} fehlen."); return None; + if not all(col in df_encoded.columns for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']): + logging.critical("FEHLER: Konsolidierte numerische Spalten fehlen.") + return None + target_column = 'Techniker_Bucket' + original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] + if not all(col in df_encoded.columns for col in original_data_cols): + logging.critical(f"FEHLER: Originaldaten-Spalten {original_data_cols} fehlen.") + return None df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy() for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter', 'Anzahl_Servicetechniker_Numeric']: - if col in df_model_ready.columns: df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') + if col in df_model_ready.columns: + df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce') df_model_ready = df_model_ready.reset_index(drop=True) - logging.info("Datenvorbereitung für Modellierung abgeschlossen."); logging.info(f"Finaler DataFrame hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten."); - logging.info(f"Anzahl Feature-Spalten: {len(feature_columns)}"); logging.info(f"Ziel-Spalte: {target_column}"); - nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum(); logging.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}"); - rows_with_nan = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().any(axis=1).sum(); logging.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature: {rows_with_nan}"); + logging.info("Datenvorbereitung für Modellierung abgeschlossen.") + logging.info(f"Finaler DataFrame hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.") + logging.info(f"Anzahl Feature-Spalten: {len(feature_columns)}") + logging.info(f"Ziel-Spalte: {target_column}") + nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum() + logging.info(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}") + rows_with_nan = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().any(axis=1).sum() + logging.info(f"Anzahl Zeilen mit mindestens einem fehlenden numerischen Feature: {rows_with_nan}") return df_model_ready