From 38e28832e0639f33d4071bbdb65ae0a5b922c1ff Mon Sep 17 00:00:00 2001 From: Floke Date: Sun, 1 Jun 2025 14:04:44 +0000 Subject: [PATCH] =?UTF-8?q?v1.7.8:=20Feature=20'is=5Fpart=5Fof=5Fgroup'=20?= =?UTF-8?q?f=C3=BCr=20ML=20&=20erweiterte=20Konzernlogik=20Plausi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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`. --- brancheneinstufung.py | 125 +++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 27 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 8e2ae926..2383651f 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -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.