v1.7.8: Feature 'is_part_of_group' für ML & erweiterte Konzernlogik Plausi

- Feature Engineering: Dynamische Erstellung des Features 'is_part_of_group' in `prepare_data_for_modeling` und `_predict_technician_bucket` basierend auf Spalten D (Parent Account Name) sowie O (System Vorschlag Parent Account) und P (Parent Vorschlag Status). Dieses Feature wird nun für das ML-Training und die Vorhersage verwendet.
- Plausibilitäts-Logik erweitert: `_check_financial_plausibility` berücksichtigt jetzt nicht nur Spalte D, sondern auch einen bestätigten Parent-Vorschlag aus Spalte O (mit P='x'), um die `INFO_KONZERN_LOGIK` für die Abweichungsflags (BJ, BK) anzuwenden. Die aufrufenden Stellen in `_process_single_row` und `run_plausibility_checks_batch` wurden angepasst, um die notwendigen Daten (O, P) an die Plausi-Funktion zu übergeben.
- Bugfix: `UnboundLocalError` für die Variable `bonus` in `serp_wikipedia_lookup` durch korrekte Initialisierung behoben.
- Bugfix: `KeyError` für "Timestamp letzte Pruefung" in `_process_single_row` durch korrekte Schreibweise des Spaltennamens-Schlüssels (mit "ue") behoben.
- Bugfix: `NameError` für `source_of_wiki_data_origin` im Konsolidierungs-Log-String in `_process_single_row` durch Verwendung eines Fallback-Wertes behoben.
- Code-Struktur: Debug-Logausgabe für den Inhalt von `current_wiki_url_r` in `_process_single_row` vor der Parent-Prüfung hinzugefügt.
- Code-Struktur: Korrektur der Limit-Anwendung und Entfernung eines fehlerhaften Code-Blocks in `run_plausibility_checks_batch`.
This commit is contained in:
2025-06-01 14:04:44 +00:00
parent 267d034feb
commit 38e28832e0

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Automatisiertes Unternehmensbewertungs-Skript - Refactoring v1.7.0
Automatisiertes Unternehmensbewertungs-Skript - Refactoring v1.7.8
Basierend auf v1.6.x - Umstrukturierung in modulare Klassen und flexibles UI.
Dieses Skript dient der automatisierten Anreicherung, Validierung und Standardisierung
@@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi
Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn).
Autor: Christian Godelmann
Version: v1.7.7
Version: v1.7.8
Hinweis zur Struktur:
Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke
@@ -107,7 +107,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Neu (Empfohlen)
# --- Globale Konfiguration Klasse ---
class Config:
"""Zentrale Konfigurationseinstellungen."""
VERSION = "v1.7.7"
VERSION = "v1.7.8"
LANG = "de" # Sprache fuer Wikipedia etc.
# ACHTUNG: SHEET_URL ist hier ein Platzhalter. Ersetzen Sie ihn durch Ihre tatsaechliche URL.
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" # <<< ERSETZEN SIE DIES!
@@ -4826,15 +4826,19 @@ class DataProcessor:
self.logger.debug(f" Zeile {row_num_in_sheet}: Führe Plausibilitäts-Checks durch (Parent D: '{parent_account_name_d}')...")
try:
plausi_input_data = {
"Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert, # Aus Schritt 3e
"Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert, # Aus Schritt 3e
"CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"), # Spalte L
"Wiki Umsatz": final_wiki_data.get('umsatz', 'k.A.'), # Spalte W (kann von Parent sein)
"CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"), # Spalte M
"Wiki Mitarbeiter": final_wiki_data.get('mitarbeiter', 'k.A.'), # Spalte X (kann von Parent sein)
"Parent Account Name": parent_account_name_d # WICHTIG: Spalte D übergeben
"Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert,
"Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert,
"CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"),
"Wiki Umsatz": final_wiki_data.get('umsatz', 'k.A.'), # oder self._get_cell_value_safe(row_data, "Wiki Umsatz")
"CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"),
"Wiki Mitarbeiter": final_wiki_data.get('mitarbeiter', 'k.A.'), # oder self._get_cell_value_safe(row_data, "Wiki Mitarbeiter")
"Parent Account Name": parent_account_name_d,
# NEU HINZUGEFÜGT:
"System Vorschlag Parent Account": self._get_cell_value_safe(row_data, "System Vorschlag Parent Account"),
"Parent Vorschlag Status": self._get_cell_value_safe(row_data, "Parent Vorschlag Status")
}
plausi_results = self._check_financial_plausibility(plausi_input_data) # Diese Methode muss D berücksichtigen
plausi_results = self._check_financial_plausibility(plausi_input_data)
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]})
updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]})
@@ -7960,9 +7964,34 @@ class DataProcessor:
parent_account_name_d_val = row_data_dict.get("Parent Account Name", "").strip()
is_konzern_tochter_laut_d = bool(parent_account_name_d_val and parent_account_name_d_val.lower() != 'k.a.')
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ NEU/ERWEITERT: Info aus Spalte O und P für Konzernlogik heranziehen +++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Diese Keys müssen im row_data_dict vorhanden sein, wenn diese Funktion aufgerufen wird!
parent_o_val = row_data_dict.get("System Vorschlag Parent Account", "").strip().lower()
parent_p_val = row_data_dict.get("Parent Vorschlag Status", "").strip().lower()
is_konzern_tochter_laut_o_und_p = bool(parent_o_val and parent_o_val != 'k.a.' and parent_p_val == 'x')
# (Das `wiki_stammt_von_parent_explizit` Flag ist optional, wenn Sie den anderen Ansatz verfolgen)
# wiki_stammt_von_parent_explizit = row_data_dict.get("Wiki Daten von Parent", False)
is_part_of_a_group_for_plausi = is_konzern_tochter_laut_d or is_konzern_tochter_laut_o_und_p # or wiki_stammt_von_parent_explizit
log_msg_group_parts = []
if is_konzern_tochter_laut_d:
self.logger.debug(f" PlausiCheck: Tochter von Konzern '{parent_account_name_d_val}' (aus Spalte D). Abweichungs-Checks CRM/Wiki werden als INFO_KONZERN_LOGIK behandelt.")
log_msg_group_parts.append(f"D='{parent_account_name_d_val}'")
if is_konzern_tochter_laut_o_und_p:
log_msg_group_parts.append(f"O/P='{parent_o_val}/{parent_p_val}'")
# if wiki_stammt_von_parent_explizit:
# log_msg_group_parts.append("WikiParentFlag=True")
if is_part_of_a_group_for_plausi:
self.logger.debug(f" PlausiCheck: Unternehmen ist Teil einer Gruppe ({'; '.join(log_msg_group_parts)}). Abweichungs-Checks CRM/Wiki werden als INFO_KONZERN_LOGIK behandelt.")
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ ENDE NEU/ERWEITERT ++++++++++++++++++++++++++++++++++++++++++++++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# --- 1. Plausibilität Finaler Umsatz (BG) ---
final_umsatz_str = row_data_dict.get("Finaler Umsatz (Wiki>CRM)", "k.A.")
@@ -8041,14 +8070,8 @@ class DataProcessor:
# Umsatz Abweichung (BJ)
if pd.notna(crm_u_abs) and pd.notna(wiki_u_abs) and crm_u_abs > 0 and wiki_u_abs > 0 :
# --- START ANPASSUNG FÜR KONZERN ---
if is_konzern_tochter_laut_d:
if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN
results["abweichung_umsatz_flag"] = "INFO_KONZERN_LOGIK"
# Begründung wird hier NICHT mehr automatisch hinzugefügt,
# da sie sonst bei jeder Konzernfirma erscheint, auch wenn alles OK ist.
# Man könnte es hinzufügen, wenn man möchte, dass es immer da steht.
# temp_begruendungen.append(f"INFO: Konzern - Umsatzabweichung CRM/Wiki nicht als Warnung.")
# --- ENDE ANPASSUNG FÜR KONZERN ---
else:
diff_umsatz = abs(crm_u_abs - wiki_u_abs)
bezugswert_umsatz = max(crm_u_abs, wiki_u_abs) if max(crm_u_abs, wiki_u_abs) > 0 else 1
@@ -8063,11 +8086,8 @@ class DataProcessor:
# Mitarbeiter Abweichung (BK)
if pd.notna(crm_m_abs_comp) and pd.notna(wiki_m_abs_comp) and crm_m_abs_comp > 0 and wiki_m_abs_comp > 0:
# --- START ANPASSUNG FÜR KONZERN ---
if is_konzern_tochter_laut_d:
if is_part_of_a_group_for_plausi: # ERWEITERTE BEDINGUNG HIER VERWENDEN
results["abweichung_ma_flag"] = "INFO_KONZERN_LOGIK"
# temp_begruendungen.append(f"INFO: Konzern - MA-Abweichung CRM/Wiki nicht als Warnung.")
# --- ENDE ANPASSUNG FÜR KONZERN ---
else:
diff_ma = abs(crm_m_abs_comp - wiki_m_abs_comp)
bezugswert_ma = max(crm_m_abs_comp, wiki_m_abs_comp) if max(crm_m_abs_comp, wiki_m_abs_comp) > 0 else 1
@@ -8183,13 +8203,19 @@ class DataProcessor:
"Finaler Umsatz (Wiki>CRM)": final_umsatz_str_konsolidiert,
"Finaler Mitarbeiter (Wiki>CRM)": final_ma_str_konsolidiert,
"CRM Umsatz": self._get_cell_value_safe(row_data, "CRM Umsatz"),
"Wiki Umsatz": self._get_cell_value_safe(row_data, "Wiki Umsatz"),
"Wiki Umsatz": final_wiki_data.get('umsatz', 'k.A.'), # oder self._get_cell_value_safe(row_data, "Wiki Umsatz")
"CRM Anzahl Mitarbeiter": self._get_cell_value_safe(row_data, "CRM Anzahl Mitarbeiter"),
"Wiki Mitarbeiter": self._get_cell_value_safe(row_data, "Wiki Mitarbeiter"),
"Parent Account Name": parent_account_name_d_val
"Wiki Mitarbeiter": final_wiki_data.get('mitarbeiter', 'k.A.'), # oder self._get_cell_value_safe(row_data, "Wiki Mitarbeiter")
"Parent Account Name": parent_account_name_d,
# NEU HINZUGEFÜGT:
"System Vorschlag Parent Account": self._get_cell_value_safe(row_data, "System Vorschlag Parent Account"),
"Parent Vorschlag Status": self._get_cell_value_safe(row_data, "Parent Vorschlag Status")
}
plausi_results = self._check_financial_plausibility(plausi_input_data)
current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_umsatz_flag", "ERR_FLAG")]]})
current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Mitarbeiter"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ma_flag", "ERR_FLAG")]]})
current_row_updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Plausibilität Umsatz/MA Ratio"] + 1)}{row_num_sheet}', 'values': [[plausi_results.get("plaus_ratio_flag", "ERR_FLAG")]]})
@@ -8930,6 +8956,9 @@ class DataProcessor:
"ma_crm": "CRM Anzahl Mitarbeiter", # Fuer Konsolidierung
"ma_wiki": "Wiki Mitarbeiter", # Fuer Konsolidierung
"techniker": "CRM Anzahl Techniker" # DIE ZIELVARIABLE (Bekannte Technikerzahl)
"parent_d_raw": "Parent Account Name", # Spalte D
"parent_o_raw": "System Vorschlag Parent Account", # Spalte O
"parent_p_raw": "Parent Vorschlag Status" # Spalte P
}
# Ueberpruefe, ob alle benoetigten Spalten-Schluessel in der COLUMN_MAP (Block 1) vorhanden sind
@@ -8979,6 +9008,35 @@ class DataProcessor:
self.logger.info(f"Benötigte Spalten fuer Modellierung ausgewaehlt und umbenannt: {list(df_subset.columns)}") # <<< GEÄNDERT
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ NEUER BLOCK: Erstellung des 'is_part_of_group' Features +++++++++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
self.logger.info("Erstelle Feature 'is_part_of_group'...")
# Sicherstellen, dass die Spalten als String behandelt werden und NaNs/Leerstrings korrekt ausgewertet werden
parent_d_series = df_subset['parent_d_raw'].astype(str).str.strip().str.lower()
parent_o_series = df_subset['parent_o_raw'].astype(str).str.strip().str.lower()
parent_p_series = df_subset['parent_p_raw'].astype(str).str.strip().str.lower()
# Bedingung 1: Parent D ist gefüllt und nicht 'k.a.'
cond1 = parent_d_series.notna() & (parent_d_series != 'k.a.') & (parent_d_series != '')
# Bedingung 2: Parent O ist gefüllt und nicht 'k.a.' UND Parent P Status ist 'x'
cond2_o = parent_o_series.notna() & (parent_o_series != 'k.a.') & (parent_o_series != '')
cond2_p = parent_p_series == 'x' # 'x' für akzeptiert
cond2 = cond2_o & cond2_p
df_subset['is_part_of_group'] = np.where(cond1 | cond2, 1, 0)
self.logger.info(f"Feature 'is_part_of_group' erstellt. {df_subset['is_part_of_group'].sum()} Unternehmen als Teil einer Gruppe markiert.")
# Überprüfen Sie die Verteilung, um sicherzustellen, dass es nicht nur Nullen oder Einsen sind
self.logger.debug(f"Verteilung von 'is_part_of_group':\n{df_subset['is_part_of_group'].value_counts(normalize=True)}")
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++ ENDE NEUER BLOCK ++++++++++++++++++++++++++++++++++++++++++++++++++++
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# --- Features konsolidieren (Umsatz, Mitarbeiter) ---
# Nutzt die globale Hilfsfunktion get_numeric_filter_value (Block 5) - ERSETZT get_valid_numeric
cols_to_process = {
@@ -9096,6 +9154,19 @@ class DataProcessor:
# .str.strip() entfernt führende/endende Leerzeichen.
df_filtered[branche_col_internal] = df_filtered[branche_col_internal].astype(str).fillna('Unbekannt').str.strip()
parent_d_val = self._get_cell_value_safe(row_data, "Parent Account Name").strip().lower()
parent_o_val = self._get_cell_value_safe(row_data, "System Vorschlag Parent Account").strip().lower()
parent_p_val = self._get_cell_value_safe(row_data, "Parent Vorschlag Status").strip().lower()
cond1_pred = bool(parent_d_val and parent_d_val != 'k.a.')
cond2_o_pred = bool(parent_o_val and parent_o_val != 'k.a.')
cond2_p_pred = parent_p_val == 'x'
cond2_pred = cond2_o_pred and cond2_p_pred
# df_single_row ist hier der DataFrame mit einer Zeile
df_single_row['is_part_of_group'] = 1 if cond1_pred | cond2_pred else 0
self.logger.debug(f" ML Pred: 'is_part_of_group' gesetzt auf {df_single_row['is_part_of_group'].iloc[0]}")
# One-Hot Encoding (pd.get_dummies)
# dummy_na=False, da wir NaNs bereits mit 'Unbekannt' gefuellt haben.