From 0d54c59ae3f11c0b83bfb15a86a5d7bcaa504702 Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 12 May 2025 19:32:36 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 212 ++++++++++++++++++------------------------ 1 file changed, 93 insertions(+), 119 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index b1e9584c..9e6db9a2 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -802,93 +802,63 @@ def extract_numeric_value(raw_value, is_umsatz=False): # Extrahiert und normalisiert Zahlenwerte fuer Vergleichslogik. # Nutzt globale Helfer: clean_text, re. def get_numeric_filter_value(value_str, is_umsatz=False): - """ - Extrahiert und normalisiert Zahlenwerte fuer die Filterlogik (Umsatz in Mio, Mitarbeiter int). - Gibt 0.0 (fuer Umsatz) oder 0 (fuer Mitarbeiter) zurueck, wenn der Wert leer, k.A., nicht numerisch ist, oder 0 ergibt. - Beachtet Einheiten (Tsd, Mio, Mrd) fuer Umsatz. - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - logger = logging.getLogger(__name__) # <<< DIESE ZEILE HINZUFÜGEN + logger = logging.getLogger(__name__) if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': - # Gibt 0 (int/float) zurueck, nicht "k.A." fuer Filterlogik - return 0.0 if is_umsatz else 0 + return 0.0 if is_umsatz else 0 # Für Filterlogik 0, wenn unbekannt raw_value_str = str(value_str).strip() - # Pruefe auf bekannte "keine Angabe" Strings - if raw_value_str.lower() in ['k.a.', 'n/a', '-']: + if raw_value_str.lower() in ['k.a.', 'n/a', '-','0','0.0','0,00','0,000']: # "0" als expliziter String auch als "unbekannt" für Filter return 0.0 if is_umsatz else 0 - try: processed_value = clean_text(raw_value_str) - if processed_value == "k.A." or processed_value.lower() in ['k.a.', 'n/a', '-']: - return 0.0 if is_umsatz else 0 # Pruefe erneut nach clean_text + if processed_value.lower() in ['k.a.', 'n/a', '-']: return 0.0 if is_umsatz else 0 - - # Entferne gaengige Praefixe/Suffixe und Spannen-Trennzeichen - processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|ueber|unter|mehr als|weniger als|bis zu)\s+', '', processed_value) # Beruecksichtige Umlaute (ue) + processed_value = re.sub(r'(?i)^\s*(ca\.?|circa|rund|etwa|ueber|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() # Nimm nur den ersten Teil bei Spannen - - # Entferne Tausendertrenner (Punkt, Apostroph) und ersetze Komma durch Punkt fuer Dezimal - processed_value_no_thousands = processed_value.replace('.', '').replace("'", "") - processed_value_final = processed_value_no_thousands.replace(',', '.') - - - # Finde die erste Sequenz von Ziffern und Punkten (die Zahl) - match = re.search(r'([\d.]+)', processed_value_final) - if not match: - # Wenn nach der Bereinigung keine Ziffern oder Punkte gefunden werden - # logger.debug(f"get_numeric_filter_value: Keine numerischen Zeichen gefunden nach Bereinigung von: '{raw_value_str}'") # Zu viel Laerm im Debug - return 0.0 if is_umsatz else 0 + processed_value = re.split(r'\s*(-|–|bis)\s*', processed_value, 1)[0].strip() + + # Tausendertrenner-Logik (vereinfacht, muss ggf. robuster werden) + temp_val_for_num = processed_value.replace("'", "") + if '.' in temp_val_for_num and ',' in temp_val_for_num: + if temp_val_for_num.rfind('.') > temp_val_for_num.rfind(','): + temp_val_for_num = temp_val_for_num.replace(',', '') + else: + temp_val_for_num = temp_val_for_num.replace('.', '').replace(',', '.') + elif ',' in temp_val_for_num: + temp_val_for_num = temp_val_for_num.replace(',', '.') + elif temp_val_for_num.count('.') > 1: # Mehrere Punkte -> Tausendertrenner + temp_val_for_num = temp_val_for_num.replace('.', '') + + match = re.search(r'([\d.\-]+)', temp_val_for_num) # Erlaube auch negative Zahlen am Anfang + if not match: return 0.0 if is_umsatz else 0 num_str = match.group(1) - try: - # Zusaetzliche Pruefungen auf ungueltige Zahlenstrings - if not num_str or num_str == '.' or num_str.count('.') > 1: - raise ValueError("Leerer oder ungueltiger Zahlenstring gefunden nach Regex Match") - # Konvertiere zu Float - num = float(num_str) - except ValueError as e: - # Wenn die Konvertierung zu Float fehlschlaegt - # logger.debug(f"Fehler bei Float-Umwandlung des extrahierten Strings '{num_str}' (aus '{raw_value_str}'): {e}") # Zu viel Laerm im Debug - return 0.0 if is_umsatz else 0 - - - # --- Einheiten-Skalierung basierend auf ORIGINALSTRING --- - # Ziel: Den Wert in die Einheit des Schwellenwerts konvertieren (Mio fuer Umsatz, Integer fuer MA). - original_lower = raw_value_str.lower(); multiplier = 1.0 - - if is_umsatz: # Umsatz (Schwellenwert in Mio) - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - num = num * 1000.0 # Konvertiere von Mrd zu Mio - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - num = num / 1000.0 # Konvertiere von Tsd zu Mio - # Wenn "Mio" oder keine Einheit, nehme num direkt (wird als Mio interpretiert) - - else: # Mitarbeiterzahl (Schwellenwert ist Integer) - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - num = num * 1000000000.0 # Konvertiere von Mrd zu Integer - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): # Beruecksichtige "mill." - num = num * 1000000.0 # Konvertiere von Mio zu Integer - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - num = num * 1000.0 # Konvertiere von Tsd zu Integer - # Wenn keine Einheit, nehme num direkt (wird als Integer interpretiert) - - # Das Ergebnis muss 0 oder positiv sein fuer die Filterlogik - result_num = num if num > 0 else 0 # Werte <= 0 zaehlen nicht + if not num_str or num_str == '.' or (num_str.count('.') > 1 and not num_str.endswith('.')): + raise ValueError("Ungültiger Zahlenstring") + + num = float(num_str) + # Einheiten-Skalierung: + # WENN is_umsatz=True, gehen wir davon aus, der Wert im Sheet ist BEREITS in Mio, + # AUSSER es steht explizit Mrd oder Tsd dabei. + original_lower = raw_value_str.lower() + if is_umsatz: - # Rueckgabe als Wert in Millionen (Float) - return result_num / 1000000.0 - else: # Mitarbeiterzahl - # Rueckgabe als ganze Zahl - return round(result_num) + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + num = num * 1000.0 # von Mrd zu Mio + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + num = num / 1000.0 # von Tsd zu Mio + # ANSONSTEN (keine Einheit oder "Mio" explizit): num ist bereits in Mio + else: # Mitarbeiter + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): num = num * 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): num = num * 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): num = num * 1000.0 + + return num if num > 0 else (0.0 if is_umsatz else 0) # Nur positive Werte, sonst 0 für Filter except Exception as e: - # Fange unerwartete Fehler ab und logge sie - logger.debug(f"Fehler in get_numeric_filter_value fuer Wert '{raw_value_str[:50]}...': {e}") - # Rueckgabe 0 bei Fehler fuer Filterlogik + logger.debug(f"Fehler in get_numeric_filter_value für Wert '{raw_value_str[:50]}...': {e}") return 0.0 if is_umsatz else 0 @@ -7906,65 +7876,69 @@ class DataProcessor: def _get_numeric_value_for_plausi(self, value_str, is_umsatz=False): - if value_str is None or pd.isna(value_str): - return np.nan - + if value_str is None or pd.isna(value_str): return np.nan raw_value_str_clean = str(value_str).strip() - # Fall 1: Explizit "unbekannt" oder leer - if raw_value_str_clean.lower() in ['', 'k.a.', 'n/a', '-']: - self.logger.debug(f"_get_numeric_value_for_plausi: Input '{raw_value_str_clean}' als 'unbekannt/leer' (NaN) interpretiert.") + if raw_value_str_clean.lower() in ['', 'k.a.', 'n/a', '-', '0', '0.0', '0,00', '0.000']: + self.logger.debug(f"_get_numeric_value_for_plausi: Input '{raw_value_str_clean}' als 'unbekannt' (NaN) interpretiert.") return np.nan - # Fall 2: Versuche, als Zahl zu interpretieren - # (Hier sollte die volle Logik von extract_numeric_value für Einheiten etc. stehen) - # Für dieses Beispiel eine vereinfachte Version, die Tausendertrenner und Komma als Dezimal behandelt: - - temp_val = clean_text(raw_value_str_clean) # Bereinigen + temp_val = clean_text(raw_value_str_clean) if temp_val.lower() in ['k.a.', 'n/a', '-']: return np.nan - - # Entferne Nicht-Zahlen-Zeichen außer Punkt und Komma und Minus am Anfang - temp_val = re.sub(r'[^\d.,\-]', '', temp_val) - # Wenn nach Bereinigung leer - if not temp_val: return np.nan - - # Behandlung von Tausenderpunkten und Dezimalkomma - if '.' in temp_val and ',' in temp_val: # z.B. 1.234,56 oder 1,234.56 - if temp_val.rfind('.') > temp_val.rfind(','): # US-Stil: 1,234.56 - temp_val = temp_val.replace(',', '') - else: # EU-Stil: 1.234,56 - temp_val = temp_val.replace('.', '').replace(',', '.') - elif ',' in temp_val: # Nur Komma: 1234,56 -> 1234.56 - temp_val = temp_val.replace(',', '.') - # Wenn nur Punkte: 1.234.567 -> 1234567 (Annahme: Tausendertrenner) - # Dies ist heikel, wenn es sich um IP-Adressen oder Versionsnummern handelt. - # Aber für Finanzdaten ist es oft so. - elif temp_val.count('.') > 1: - temp_val = temp_val.replace('.', '') + temp_val = re.sub(r'(?i)^\s*(ca\.?|circa|...)\s+', '', temp_val) # Gekürzt für Lesbarkeit + temp_val = re.sub(r'[€$£¥]', '', temp_val).strip() + temp_val = re.split(r'\s*(-|–|bis)\s*', temp_val, 1)[0].strip() + # Tausendertrenner-Logik (wie oben) + temp_val_for_num = temp_val.replace("'", "") + if '.' in temp_val_for_num and ',' in temp_val_for_num: + if temp_val_for_num.rfind('.') > temp_val_for_num.rfind(','): temp_val_for_num = temp_val_for_num.replace(',', '') + else: temp_val_for_num = temp_val_for_num.replace('.', '').replace(',', '.') + elif ',' in temp_val_for_num: temp_val_for_num = temp_val_for_num.replace(',', '.') + elif temp_val_for_num.count('.') > 1: temp_val_for_num = temp_val_for_num.replace('.', '') + + match = re.search(r'([\d.\-]+)', temp_val_for_num) + if not match: + self.logger.debug(f"_get_numeric_value_for_plausi: Kein numerischer Match in '{temp_val_for_num}' (von '{raw_value_str_clean}')") + return np.nan + + num_str = match.group(1) try: - num = float(temp_val) - - # Einheiten-Skalierung (vereinfacht aus extract_numeric_value) + if not num_str or num_str == '.' or (num_str.count('.') > 1 and not num_str.endswith('.')): + raise ValueError("Ungültiger Zahlenstring") + num = float(num_str) # Das ist die Zahl, wie sie im String steht (ggf. schon Mio) + original_lower = raw_value_str_clean.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 - - final_num = num * multiplier + final_num_absolute = num # Initialannahme: Zahl ist in der Grundeinheit (außer wenn Umsatz und keine Einheit) - # Gemäß Ihrer Regel: Wenn der *ursprüngliche String* "0" (oder Ähnliches) war, soll es als "unbekannt" (NaN) gelten. - if final_num == 0.0 and raw_value_str_clean.strip() in ['0', '0.0', '0,0', '0.00', '0,000', '0.000']: - self.logger.debug(f"_get_numeric_value_for_plausi: Input '{raw_value_str_clean}' ist explizit '0', als 'unbekannt' (NaN) interpretiert.") - return np.nan + if is_umsatz: + # WENN der String NUR eine Zahl ist (z.B. "173"), NEHMEN WIR AN, ES SIND BEREITS MILLIONEN + # und wir wollen den absoluten Wert in Euro für die Plausi-Checks. + if not any(kw in original_lower for kw in ['mrd', 'milliarden', 'billion', 'tsd', 'tausend']): + # Keine explizite Einheit gefunden, und es ist Umsatz -> interpretiere als Mio + final_num_absolute = num * 1000000.0 + elif re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + final_num_absolute = num * 1000000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + final_num_absolute = num * 1000.0 + # Wenn "mio" oder "millionen" explizit drinsteht, ist `num` bereits der Mio-Wert, + # also multiplizieren wir mit 1 Mio, um auf Euro zu kommen. + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): + final_num_absolute = num * 1000000.0 + # Sonst (falls es eine andere, nicht behandelte Einheit war), bleibt es `num` + # Dies sollte aber durch die erste Bedingung (nur Zahl -> Mio) abgedeckt sein. + else: # Mitarbeiter (bereits absolute Zahl erwartet, außer bei expliziten Einheiten) + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): final_num_absolute = num * 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): final_num_absolute = num * 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): final_num_absolute = num * 1000.0 - # self.logger.debug(f"_get_numeric_value_for_plausi: Input '{raw_value_str_clean}' erfolgreich zu {final_num} konvertiert.") - return final_num # Gibt die Zahl zurück, auch wenn es 0.0 ist (aus z.B. "0 Tsd") + # Ihre Regel: String "0" wurde oben zu NaN. + # Wenn final_num_absolute jetzt 0 ist, war es eine Berechnung wie "0 Tsd". + return final_num_absolute - except ValueError: - self.logger.debug(f"_get_numeric_value_for_plausi: Konnte '{raw_value_str_clean}' (verarbeitet als '{temp_val}') nicht zu float konvertieren.") + except ValueError as e: + self.logger.debug(f"_get_numeric_value_for_plausi: ValueError bei Konvertierung von '{num_str}' (von '{raw_value_str_clean}'): {e}") return np.nan