From 90bdc7edfb1b9ba6bc0b2b068af0f2e67dd71e42 Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 13 May 2025 10:27:44 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 196 ++++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 73 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index aaf0e133..43f6d7b9 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -804,61 +804,101 @@ def extract_numeric_value(raw_value, is_umsatz=False): def get_numeric_filter_value(value_str, is_umsatz=False): logger = logging.getLogger(__name__) if value_str is None or pd.isna(value_str) or str(value_str).strip() == '': - return 0.0 if is_umsatz else 0 # Für Filterlogik 0, wenn unbekannt + return 0.0 if is_umsatz else 0 - raw_value_str = str(value_str).strip() - 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 + raw_value_str_original = str(value_str).strip() # Original für Einheiten-Keywords behalten + + # "0" und "k.A." als expliziter String wird für Filterzwecke wie 0 behandelt + if raw_value_str_original.lower() in ['k.a.', 'n/a', '-', '0', '0.0', '0,00', '0,000']: return 0.0 if is_umsatz else 0 try: - processed_value = clean_text(raw_value_str) - if processed_value.lower() in ['k.a.', 'n/a', '-']: return 0.0 if is_umsatz else 0 + # Bereinigung für die Zahlenextraktion + processed_value = clean_text(raw_value_str_original) # clean_text ist Ihre globale Funktion + if processed_value.lower() in ['k.a.', 'n/a', '-']: + return 0.0 if is_umsatz else 0 + # Präfixe und Währungssymbole entfernen 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() + # Nur den ersten Teil bei Spannen nehmen (z.B. "100 - 200" -> "100") 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('.', '') + # Umgang mit Tausendertrennern (Punkt) und Dezimalkomma + # Ziel: Einen String erhalten, der von float() korrekt als Zahl interpretiert wird (Dezimaltrenner ist Punkt) - 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_extraction_str = processed_value.replace("'", "") # Apostroph als Tausendertrenner entfernen - num_str = match.group(1) - 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) + # Wenn Punkt und Komma vorkommen -> versuche Format zu erkennen + if '.' in num_extraction_str and ',' in num_extraction_str: + if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US-Format: 1,234.56 -> Kommas entfernen + num_extraction_str = num_extraction_str.replace(',', '') + else: # EU-Format: 1.234,56 -> Punkte entfernen, Komma zu Punkt + num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') + elif ',' in num_extraction_str: # Nur Kommas: 1234,56 -> 1234.56 + num_extraction_str = num_extraction_str.replace(',', '.') + elif '.' in num_extraction_str: # Nur Punkte + # Wenn der String ein typisches Tausenderformat hat (z.B. 1.234 oder 1.234.567), Punkte entfernen + if re.fullmatch(r'\d{1,3}(\.\d{3})+', num_extraction_str): # z.B. 4.380 oder 17.800 + num_extraction_str = num_extraction_str.replace('.', '') + # Ansonsten (z.B. "123.45") ist der Punkt wahrscheinlich ein Dezimalpunkt und bleibt. + # Wenn mehrere Punkte und es ist kein Tausenderformat (z.B. 1.2.3), wird es problematisch + # Hier ist eine einfache Annahme: Wenn mehr als ein Punkt und kein Tausenderformat, ist es ungültig. + elif num_extraction_str.count('.') > 1: + logger.debug(f"get_numeric_filter_value: Mehrere Punkte in '{num_extraction_str}', die nicht als Tausendertrenner erkannt wurden. Interpretiere als ungültig.") + return 0.0 if is_umsatz else 0 - # 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() + + # Extrahiere die Zahl (kann jetzt einen Dezimalpunkt enthalten) + match = re.search(r'([\d.\-]+)', num_extraction_str) # Erlaube Dezimalpunkt und optionales Minus + if not match: + # logger.debug(f"get_numeric_filter_value: Kein numerischer Match in '{num_extraction_str}' (aus '{raw_value_str_original}')") + return 0.0 if is_umsatz else 0 + + num_str_for_float = match.group(1) + # Finale Prüfung des extrahierten Zahlenstrings + if not num_str_for_float or \ + num_str_for_float == '.' or \ + num_str_for_float == '-' or \ + (num_str_for_float.count('.') > 1) or \ + (num_str_for_float.count('-') > 1) or \ + (num_str_for_float.count('-') == 1 and not num_str_for_float.startswith('-')): + # logger.debug(f"get_numeric_filter_value: Ungültiger Zahlenstring '{num_str_for_float}' nach Regex.") + return 0.0 if is_umsatz else 0 + + num_as_float = float(num_str_for_float) + + # Einheiten-Skalierung basierend auf dem ORIGINALSTRING raw_value_str_original + # Annahme: Wenn is_umsatz=True, ist num_as_float bereits in Millionen, es sei denn, + # es gibt explizite Einheiten wie "Mrd" oder "Tsd" im Originalstring. + + scaled_num = num_as_float + original_lower = raw_value_str_original.lower() + if is_umsatz: if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - num = num * 1000.0 # von Mrd zu Mio + scaled_num = num_as_float * 1000.0 # Der num_as_float war die Zahl vor Mrd, jetzt ist es 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 + scaled_num = num_as_float / 1000.0 # Der num_as_float war die Zahl vor Tsd, jetzt ist es Mio. + # Ansonsten ist num_as_float bereits der Wert in Millionen + else: # Mitarbeiter (absolute Zahl erwartet, außer bei expliziten Einheiten) + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + scaled_num = num_as_float * 1000000000.0 + elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): + scaled_num = num_as_float * 1000000.0 + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + scaled_num = num_as_float * 1000.0 - return num if num > 0 else (0.0 if is_umsatz else 0) # Nur positive Werte, sonst 0 für Filter + # Für die Filterlogik nur positive Werte zurückgeben, sonst 0 + return scaled_num if scaled_num > 0 else (0.0 if is_umsatz else 0) - except Exception as e: - logger.debug(f"Fehler in get_numeric_filter_value für Wert '{raw_value_str[:50]}...': {e}") + except ValueError as e: # Fehler bei float() Konvertierung + logger.debug(f"get_numeric_filter_value: ValueError '{e}' bei Konvertierung von '{num_str_for_float if 'num_str_for_float' in locals() else raw_value_str_original[:30]}...'") + return 0.0 if is_umsatz else 0 + except Exception as e_general: # Andere unerwartete Fehler + logger.error(f"Unerwarteter Fehler in get_numeric_filter_value für '{raw_value_str_original[:50]}...': {e_general}") + logger.debug(traceback.format_exc()) return 0.0 if is_umsatz else 0 @@ -7879,66 +7919,76 @@ class DataProcessor: if value_str is None or pd.isna(value_str): return np.nan raw_value_str_clean = str(value_str).strip() + # Explizit "0" und andere "unbekannt" Strings als NaN 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.") + self.logger.debug(f"PlausiParse: Input '{raw_value_str_clean}' -> NaN (als 'unbekannt' interpretiert).") return np.nan temp_val = clean_text(raw_value_str_clean) if temp_val.lower() in ['k.a.', 'n/a', '-']: return np.nan - 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() + temp_val = re.sub(r'(?i)^\s*(ca\.?|circa|etwa|rund|ueber|unter|mehr als|weniger als|bis zu)\s+', '', temp_val) + temp_val = re.sub(r'[€$£¥]', '', temp_val).strip() # Währungssymbole entfernen + temp_val = re.split(r'\s*(-|–|bis)\s*', temp_val, 1)[0].strip() # Nur ersten Teil bei Spannen - # 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('.', '') + # Robuste Behandlung von Tausendertrennern (Punkte) und Dezimalkomma + # Entferne zuerst alle Punkte, die als Tausendertrenner dienen könnten + # Ein Punkt ist Tausendertrenner, wenn rechts davon 3 Ziffern und dann ein weiteres Nicht-Ziffer-Zeichen (oder Ende) oder ein weiteres Komma steht + # Oder wenn mehrere Punkte vorhanden sind und kein Komma. - match = re.search(r'([\d.\-]+)', temp_val_for_num) + # Vereinfachte Tausendertrenner-Logik, die für Fälle wie "4.380" und "17.800" funktionieren sollte: + # Annahme: Wenn ein Punkt vorkommt und kein Komma, oder das letzte Komma vor dem letzten Punkt steht, + # dann sind Punkte Tausendertrenner. + + cleaned_num_str = temp_val + if '.' in cleaned_num_str and ',' in cleaned_num_str: + if cleaned_num_str.rfind('.') > cleaned_num_str.rfind(','): # US-Stil: 1,234.56 -> Kommas entfernen + cleaned_num_str = cleaned_num_str.replace(',', '') + else: # EU-Stil: 1.234,56 -> Punkte entfernen, Komma zu Punkt + cleaned_num_str = cleaned_num_str.replace('.', '').replace(',', '.') + elif ',' in cleaned_num_str: # Nur Komma: 1234,56 -> 1234.56 + cleaned_num_str = cleaned_num_str.replace(',', '.') + elif '.' in cleaned_num_str: # Nur Punkte: + # Wenn es aussieht wie x.xxx (z.B. 4.380, 17.800), entferne den Punkt + if re.match(r'^\d{1,3}(\.\d{3})+$', cleaned_num_str): # Matches 1.234 or 1.234.567 etc. + cleaned_num_str = cleaned_num_str.replace('.', '') + # Ansonsten ist es ein Dezimalpunkt (z.B. 123.45) - bleibt bestehen + + match = re.search(r'([\d.\-]+)', cleaned_num_str) # Erlaube Dezimalpunkt und Minus 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}')") + self.logger.debug(f"PlausiParse: Kein numerischer Match in '{cleaned_num_str}' (von '{raw_value_str_clean}') -> NaN.") return np.nan - num_str = match.group(1) + num_str_from_regex = match.group(1) try: - 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) + # Vermeide Interpretation von "...." als Zahl + if not num_str_from_regex or num_str_from_regex == '.' or (num_str_from_regex.count('.') > 1 and not num_str_from_regex.endswith('.')): + raise ValueError("Ungültiger Zahlenstring nach Regex") + + num = float(num_str_from_regex) # Das ist die Zahl, wie sie im String steht (z.B. 173, 4380, 17800) original_lower = raw_value_str_clean.lower() - final_num_absolute = num # Initialannahme: Zahl ist in der Grundeinheit (außer wenn Umsatz und keine Einheit) + final_num_absolute = num 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 + # Input ist bereits in Mio. €, es sei denn, es gibt explizite andere Einheiten + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + final_num_absolute = num * 1000000000.0 # z.B. "2 Mrd" -> 2 * 10^9 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) + final_num_absolute = num * 1000.0 # z.B. "500 Tsd" -> 500 * 10^3 + else: # Annahme: num ist bereits in Mio, konvertiere zu absolutem Euro-Wert + final_num_absolute = num * 1000000.0 + else: # Mitarbeiter (absolute Zahl, es sei denn, explizite 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 - # Ihre Regel: String "0" wurde oben zu NaN. - # Wenn final_num_absolute jetzt 0 ist, war es eine Berechnung wie "0 Tsd". + # "0" (als String) wurde oben schon zu NaN. + # Wenn final_num_absolute jetzt numerisch 0 ist, war es z.B. "0 Tsd". return final_num_absolute 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}") + self.logger.debug(f"PlausiParse: ValueError '{e}' bei Konvertierung von '{num_str_from_regex}' (von '{raw_value_str_clean}') -> NaN.") return np.nan