From ca49e89d32fdc788044eb1a82ab4adcd14e369a8 Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 25 May 2025 10:43:07 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 189 ++++++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 82 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 5514f2e5..5e4d608b 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -715,92 +715,117 @@ def fuzzy_similarity(str1, str2): # Extrahiert und normalisiert Zahlenwerte aus Strings. # Nutzt globale Helfer: clean_text, re. def extract_numeric_value(raw_value, is_umsatz=False): - """ - Extrahiert und normalisiert Zahlenwerte (Umsatz in Mio, Mitarbeiter). - Berücksichtigt Tausendertrenner (Punkt, Apostroph), Dezimaltrenner (Komma), Einheiten (Tsd, Mio, Mrd) - und gaengige Praefixe/Suffixe. Gibt "k.A." zurueck, wenn nicht extrahierbar oder <= 0. - """ - # Verwenden Sie logger, da das Logging jetzt konfiguriert ist - logger = logging.getLogger(__name__) # <<< DIESE ZEILE HINZUFÜGEN - if not raw_value: return "k.A." - raw_value_str = str(raw_value).strip() - # Pruefe auf bekannte "keine Angabe" Strings oder 0 als Text - if not raw_value_str or raw_value_str.lower() in ['k.a.', 'n/a', '-']: - return "k.A." # 0 als Text ist hier wie k.A. - - - # Bereinigungsschritte aehnlich wie in clean_text und vorheriger Implementierung - processed_value = clean_text(raw_value_str) - if processed_value == "k.A." or processed_value.lower() in ['k.a.', 'n/a', '-']: - return "k.A." # Pruefe erneut nach clean_text - - - # logger.debug(f"extract_numeric_value: Verarbeite Wert: '{raw_value_str}' -> '{processed_value}' (is_umsatz={is_umsatz})") # Zu viel Laerm im Debug - - # Entferne gaengige Praefixe/Suffixe und Spannen-Trennzeichen (z.B. "ca. ", "ueber ", "100-200", "€") - 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'[€$£¥]', '', processed_value).strip() # Entferne Waehrungssymbole - 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 - # Reihenfolge ist wichtig: Erst Punkte/Apostrophe entfernen, dann Komma durch Punkt ersetzen - 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"extract_numeric_value: Keine numerischen Zeichen gefunden nach Bereinigung von: '{raw_value_str}'") + logger = logging.getLogger(__name__ + ".extract_numeric_value") # Logger für diese Funktion + if raw_value is None or pd.isna(raw_value): + return "k.A." + + raw_value_str_original = str(raw_value).strip() + if not raw_value_str_original or raw_value_str_original.lower() in ['k.a.', 'n/a', '-']: return "k.A." - num_str = match.group(1) + # Spezifische Behandlung für "0" gemäß Ihrer Regel: Wenn der Input nur "0" ist, + # dann ist es für die *Extraktion* erstmal eine 0, die dann in der + # *Konsolidierung* ggf. zu "k.A." wird, wenn keine bessere Quelle da ist. + # Für die Plausi-Checks wird String "0" dann als NaN (unbekannt) interpretiert. + # Hier lassen wir "0" als numerische 0 durch, wenn es die einzige Zahl ist. + try: - # Zusaetzliche Pruefungen auf ungueltige Zahlenstrings (z.B. nur '.', '..', '.1.') - if not num_str or num_str == '.' or num_str.count('.') > 1: - # Wenn der String leer ist, nur ein Punkt, oder mehr als ein Dezimalpunkt hat - raise ValueError("Leerer oder ungueltiger Zahlenstring gefunden nach Regex Match") - # Konvertiere den extrahierten String zu einem 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}") + # Schritt 1: Grundlegende Textbereinigung + processed_value = clean_text(raw_value_str_original) # Ihre globale clean_text Funktion + if processed_value.lower() in ['k.a.', 'n/a', '-']: + return "k.A." + + # Schritt 2: Präfixe, Suffixe, Währungssymbole und Spannen 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'[€$£¥ CHF]', '', processed_value, flags=re.IGNORECASE).strip() # CHF hinzugefügt + processed_value = re.split(r'\s*(-|–|bis)\s*', processed_value, 1)[0].strip() + + # Schritt 3: Klammerinhalte entfernen (z.B. Jahreszahlen) + num_extraction_str = re.sub(r'\(.*?\)', '', processed_value).strip() + + # Schritt 4: Apostrophe und Leerzeichen zwischen Ziffern entfernen + num_extraction_str = num_extraction_str.replace("'", "") + num_extraction_str = re.sub(r'(?<=\d)\s+(?=\d)', '', num_extraction_str) # Entfernt Leerzeichen nur zwischen Ziffern + + if not num_extraction_str: return "k.A." + + # Schritt 5: Punkte und Kommas als Tausender-/Dezimaltrennzeichen standardisieren + has_dot = '.' in num_extraction_str + has_comma = ',' in num_extraction_str + + if has_dot and has_comma: + if num_extraction_str.rfind('.') > num_extraction_str.rfind(','): # US-Stil: 1,234.56 + num_extraction_str = num_extraction_str.replace(',', '') + else: # EU-Stil: 1.234,56 + num_extraction_str = num_extraction_str.replace('.', '').replace(',', '.') + elif has_comma: # Nur Kommas + # Wenn es klar wie ein Dezimalkomma aussieht (z.B. ",dd" am Ende, nur ein Komma) + # und nicht wie ein Tausender-Komma (z.B. "8,300" -> soll 8300 werden) + if num_extraction_str.count(',') == 1 and re.search(r',\d{1,2}$', num_extraction_str) and not re.search(r',\d{3}(,|\s|\Z)', num_extraction_str): + num_extraction_str = num_extraction_str.replace(',', '.') + else: # Ansonsten sind Kommas Tausendertrenner (z.B. "8,300" -> "8300") + num_extraction_str = num_extraction_str.replace(',', '') + elif has_dot: # Nur Punkte + # Wenn es klar wie ein Dezimalpunkt aussieht (z.B. ".dd" am Ende, nur ein Punkt) + if num_extraction_str.count('.') == 1 and re.search(r'\.\d{1,2}$', num_extraction_str) and not re.search(r'\.\d{3}(?!\d)', num_extraction_str): + pass # Punkt ist Dezimal, bleibt + else: # Ansonsten sind Punkte Tausendertrenner (z.B. "4.380" -> "4380") + num_extraction_str = num_extraction_str.replace('.', '') + + # Schritt 6: Finale Validierung und Konvertierung zu float + if not re.fullmatch(r'-?\d+(\.\d+)?', num_extraction_str): + logger.debug(f"Kein gültiger numerischer String nach Trennzeichenbehandlung: '{num_extraction_str}' (Original: '{raw_value_str_original}')") + return "k.A." + + num_as_float = float(num_extraction_str) + + # Schritt 7: Einheiten-Skalierung + scaled_num = num_as_float + original_lower = raw_value_str_original.lower() # Verwende den ursprünglichen String für Keyword-Suche + + # Für Wiki-Umsatz: Ziel ist Wert in Millionen + if is_umsatz: + if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): + scaled_num = num_as_float * 1000.0 # z.B. "1,636 Mrd" -> num_as_float=1.636 -> 1636 (Mio) + elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): + scaled_num = num_as_float / 1000.0 # z.B. "500 Tsd" -> num_as_float=500 -> 0.5 (Mio) + # Wenn keine Einheit wie Mrd/Tsd explizit da ist, wird angenommen, dass num_as_float + # bereits den Wert in der im Sheet gewünschten Einheit (Mio für Umsatz) darstellt, + # *falls die Zahl aus einer Quelle stammt, die bereits in Mio ist (wie CRM)*. + # Bei Wikipedia ist das nicht immer der Fall. Wenn "Mio" explizit da steht, ist es gut. + # Wenn nur eine Zahl da steht (z.B. "4380" nach Bereinigung von "4.380"), + # müssen wir entscheiden, ob das schon Mio sind oder die Grundeinheit. + # Da die Spalte S "Wiki Umsatz" als "normalisiert in Mio. €" definiert ist, + # sollte diese Funktion einen Wert zurückgeben, der Mio. € darstellt. + # Wenn also die Infobox "4,869 billion" (Barilla) hat, wird `num_as_float` zu 4.869. + # Der `re.search` für Mrd/billion wird das erkennen und `scaled_num` zu `4869.0` (Mio) machen. + # Wenn die Infobox "1.636 Mrd" hat, wird `num_as_float` zu 1.636, `scaled_num` zu 1636 (Mio). + # Wenn die Infobox nur "4380" hätte (ohne Mio/Mrd), würde `num_as_float` 4380 sein und `scaled_num` 4380 (Mio). + # Das scheint die korrekte Interpretation für die Wiki-Spalte zu sein. + + else: # Mitarbeiter (absolute Zahl) + 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 + + # Runde auf Ganzzahl für die Ausgabe im Sheet, aber nur wenn > 0 + if scaled_num > 0: + return str(int(round(scaled_num))) + elif scaled_num == 0: # Wenn das Ergebnis exakt 0 ist (z.B. aus "0 Tsd") + return "0" + else: # Negativ oder Fehler + return "k.A." + + except ValueError as e: # Fehler bei float() + logger.debug(f"extract_numeric_value: ValueError '{e}' bei Konvertierung von '{num_extraction_str if 'num_extraction_str' in locals() and isinstance(num_extraction_str, str) else raw_value_str_original[:30]}...'") + return "k.A." + except Exception as e_general: + logger.error(f"Unerwarteter Fehler in extract_numeric_value für '{raw_value_str_original[:50]}...': {e_general}") + logger.debug(traceback.format_exc()) return "k.A." - - # --- Einheiten-Skalierung basierend auf ORIGINALSTRING --- - # Pruefe den originalen Wert (kleingeschrieben) auf Einheiten-Keywords - original_lower = raw_value_str.lower() - multiplier = 1.0 - - # Pruefe auf Mrd, Mio, Tsd Keywords und setze den entsprechenden Multiplikator - if re.search(r'\bmrd\s*\b|\bmilliarden\s*\b|\bbillion\s*\b', original_lower): - multiplier = 1000000000.0 - # logger.debug(" -> Einheit: Mrd gefunden") # Zu viel Laerm im Debug - elif re.search(r'\bmio\s*\b|\bmillionen\s*\b|\bmill[.]?\s*\b', original_lower): # Beruecksichtige "mill." - multiplier = 1000000.0 - # logger.debug(" -> Einheit: Mio gefunden") - elif re.search(r'\btsd\s*\b|\btausend\s*\b', original_lower): - multiplier = 1000.0 - # logger.debug(" -> Einheit: Tsd gefunden") - - # Wende den Multiplikator auf die Zahl an - num = num * multiplier - - # Konvertiere zu Zielformat und runde ggf. - # Rueckgabe als String, wie im Sheet erwartet - # Stellen Sie sicher, dass nur positive Werte zurueckgegeben werden (<= 0 ist wie k.A.) - if is_umsatz: - # Umsatz wird in Millionen € gespeichert (gerundet auf ganze Mio) - # Rueckgabe als String - umsatz_mio = round(num / 1000000.0) - return str(int(umsatz_mio)) if umsatz_mio > 0 else "k.A." # Nur positive Ergebnisse > 0 - else: - # Mitarbeiterzahl wird als ganze Zahl gespeichert (gerundet) - # Rueckgabe als String - mitarbeiter_int = round(num) - return str(int(mitarbeiter_int)) if mitarbeiter_int > 0 else "k.A." # Nur positive Ergebnisse > 0 # --- Numerische Extraktion fuer FILTERLOGIK (gibt 0 statt k.A. zurueck) ---