This commit is contained in:
2025-05-25 10:43:07 +00:00
parent 206f6dded7
commit ca49e89d32

View File

@@ -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) ---