v1.6.2: Bereite Techniker-Modell vor & korrigiere/ergänze Modi (Alignment, Args)
- Füge neuen Modus `--mode alignment` hinzu, um die Header-Definitionen (Zeilen 1-5) über die Funktion `alignment_demo` ins Hauptblatt zu schreiben (inkl. Sicherheitsabfrage). - Korrigiere das Kommandozeilenargument für das Zeilenlimit von `--row_limit` zu `--limit` im `argparse`-Setup in `main`. - Verbessere die `main`-Funktion, um interaktive `input()`-Abfragen für Modus und Limit nur dann zu stellen, wenn die entsprechenden Argumente nicht über die Kommandozeile bereitgestellt wurden (verhindert Fehler bei `nohup`). Füge Fehlerbehandlung für `input()` hinzu. - Integriere die neue Funktion `prepare_data_for_modeling` zur Aufbereitung der Daten für das geplante Decision-Tree-Modell zur Technikerschätzung (Funktion wird in den bestehenden Modi noch nicht aufgerufen).
This commit is contained in:
@@ -30,7 +30,13 @@ import gender_guesser.detector as gender
|
||||
from urllib.parse import urlparse, urlencode
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import numpy as np # Für NaN
|
||||
import numpy as np
|
||||
from sklearn.model_selection import train_test_split, GridSearchCV
|
||||
from sklearn.impute import SimpleImputer
|
||||
from sklearn.tree import DecisionTreeClassifier, export_text
|
||||
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
|
||||
import json # Zum Speichern der Muster als JSON
|
||||
import pickle # Zum Speichern des trainierten Modells und Imputers
|
||||
|
||||
# Optional: tiktoken für Token-Zählung (Modus 8)
|
||||
try:
|
||||
@@ -139,6 +145,227 @@ COLUMN_MAP = {
|
||||
"Website Zusammenfassung": 44 # AS
|
||||
}
|
||||
|
||||
# Annahme: COLUMN_MAP ist global definiert und enthält mindestens:
|
||||
# "CRM Name", "CRM Branche", "CRM Umsatz", "Wiki Umsatz",
|
||||
# "CRM Anzahl Mitarbeiter", "Wiki Mitarbeiter", "CRM Anzahl Techniker" (oder wo immer die bekannte Technikerzahl steht)
|
||||
|
||||
# Stelle sicher, dass die globale COLUMN_MAP verfügbar ist
|
||||
# Beispielhafte Definition (bitte an deine Spalten anpassen!)
|
||||
# COLUMN_MAP = { ... dein komplettes Mapping ... }
|
||||
|
||||
def prepare_data_for_modeling(sheet_handler):
|
||||
"""
|
||||
Lädt Daten aus dem Google Sheet, bereitet sie für das Decision Tree Modell vor:
|
||||
- Wählt relevante Spalten aus.
|
||||
- Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität).
|
||||
- Filtert nach gültiger Technikerzahl (> 0).
|
||||
- Erstellt die Zielvariable (Techniker-Bucket).
|
||||
- Bereitet Features auf (One-Hot Encoding für Branche).
|
||||
- Behält NaNs in numerischen Features für spätere Imputation.
|
||||
|
||||
Args:
|
||||
sheet_handler (GoogleSheetHandler): Instanz mit geladenen Sheet-Daten.
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: Vorbereiteter DataFrame für Training/Test-Split,
|
||||
oder None bei Fehlern.
|
||||
"""
|
||||
debug_print("Starte Datenvorbereitung für Modellierung...")
|
||||
|
||||
try:
|
||||
# --- 1. Daten laden & Spalten auswählen ---
|
||||
all_data = sheet_handler.get_all_data_with_headers()
|
||||
if len(all_data) <= 5: # Annahme: 5 Header-Zeilen
|
||||
debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.")
|
||||
return None
|
||||
headers = all_data[0] # Nimm die erste Zeile als Header für Pandas
|
||||
data_rows = all_data[5:] # Daten ohne die ersten 5 Header-Zeilen
|
||||
|
||||
# Erstelle DataFrame
|
||||
df = pd.DataFrame(data_rows, columns=headers)
|
||||
debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")
|
||||
|
||||
# Wähle benötigte Spalten aus ( passe die Schlüssel an deine COLUMN_MAP an!)
|
||||
required_cols_keys = [
|
||||
"CRM Name", # Zur Identifikation, wird später entfernt
|
||||
"CRM Branche",
|
||||
"CRM Umsatz",
|
||||
"Wiki Umsatz",
|
||||
"CRM Anzahl Mitarbeiter",
|
||||
"Wiki Mitarbeiter",
|
||||
"CRM Anzahl Techniker" # ÄNDERE DIESEN SCHLÜSSEL, falls die bekannte Zahl woanders steht!
|
||||
]
|
||||
|
||||
# Finde die tatsächlichen Spaltennamen aus den Headern basierend auf COLUMN_MAP Beschreibung (Zeile 4)
|
||||
# ODER verwende direkt die Spaltennamen, wenn sie stabil sind.
|
||||
# Hier vereinfacht angenommen, dass die Schlüssel oben die Spaltennamen sind:
|
||||
try:
|
||||
# Konvertiere Spaltennamen aus COLUMN_MAP zu echten Spaltennamen im DataFrame (falls nötig)
|
||||
# Dies ist ein Platzhalter - im echten Code müsstest du die Header-Zeilen parsen
|
||||
# oder dich darauf verlassen, dass die Schlüssel oben die exakten Spaltennamen sind.
|
||||
df_subset = df[required_cols_keys].copy() # Kopie erstellen, um SettingWithCopyWarning zu vermeiden
|
||||
except KeyError as e:
|
||||
debug_print(f"FEHLER: Benötigte Spalte nicht im DataFrame gefunden: {e}. Verfügbare Spalten: {list(df.columns)}")
|
||||
return None
|
||||
|
||||
debug_print(f"Benötigte Spalten ausgewählt.")
|
||||
|
||||
# --- 2. Features konsolidieren (Umsatz, Mitarbeiter) ---
|
||||
# Hilfsfunktion zur Validierung und Konvertierung
|
||||
def get_valid_numeric(value_str):
|
||||
if value_str is None or pd.isna(value_str): return np.nan
|
||||
try:
|
||||
# Versuche direkt float zu konvertieren
|
||||
val = float(value_str)
|
||||
return val if val > 0 else np.nan # Nur Werte > 0 sind gültig
|
||||
except (ValueError, TypeError):
|
||||
# Wenn nicht direkt float, versuche es über extract_numeric_value
|
||||
# Diese Funktion muss dafür angepasst werden, float oder np.nan zurückzugeben
|
||||
# num_val_str = extract_numeric_value(str(value_str), is_umsatz=True) # Bsp. Umsatz
|
||||
# if num_val_str != "k.A.":
|
||||
# try:
|
||||
# val = float(num_val_str)
|
||||
# return val if val > 0 else np.nan
|
||||
# except ValueError: return np.nan
|
||||
# else: return np.nan
|
||||
# --- VEREINFACHUNG für jetzt: Nur direkt konvertierbare Werte ---
|
||||
cleaned_str = re.sub(r'[^\d.,]', '', str(value_str)).replace(',', '.') # Einfache Reinigung
|
||||
try:
|
||||
val = float(cleaned_str)
|
||||
return val if val > 0 else np.nan
|
||||
except ValueError:
|
||||
return np.nan
|
||||
|
||||
|
||||
# Konvertiere Quellen-Spalten und wende Priorisierung an
|
||||
cols_to_process = {
|
||||
'Umsatz': ('Wiki Umsatz', 'CRM Umsatz', 'Finaler_Umsatz'),
|
||||
'Mitarbeiter': ('Wiki Mitarbeiter', 'CRM Anzahl Mitarbeiter', 'Finaler_Mitarbeiter')
|
||||
}
|
||||
|
||||
for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items():
|
||||
debug_print(f"Verarbeite '{base_name}' (Wiki: {wiki_col}, CRM: {crm_col})...")
|
||||
wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric)
|
||||
crm_numeric = df_subset[crm_col].apply(get_valid_numeric)
|
||||
|
||||
# Priorisierung: Wiki > CRM
|
||||
df_subset[final_col] = np.where(
|
||||
wiki_numeric.notna() & (wiki_numeric > 0), # Wenn Wiki gültig
|
||||
wiki_numeric,
|
||||
np.where(
|
||||
crm_numeric.notna() & (crm_numeric > 0), # Sonst, wenn CRM gültig
|
||||
crm_numeric,
|
||||
np.nan # Sonst NaN
|
||||
)
|
||||
)
|
||||
# Logge, wie viele Werte gefunden wurden
|
||||
debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.")
|
||||
|
||||
# Entferne die Originalspalten (optional)
|
||||
# df_subset = df_subset.drop(columns=[wiki_col, crm_col])
|
||||
|
||||
# --- 3. Zielvariable vorbereiten (Technikerzahl) ---
|
||||
techniker_col = "CRM Anzahl Techniker" # ÄNDERE DAS WENN NÖTIG!
|
||||
debug_print(f"Verarbeite Zielvariable '{techniker_col}'...")
|
||||
|
||||
# Konvertiere zu Numerisch (Fehler -> NaN)
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce')
|
||||
|
||||
# Filtere Zeilen: Behalte nur die mit gültiger, positiver Technikerzahl
|
||||
initial_rows = len(df_subset)
|
||||
df_filtered = df_subset[
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'].notna() &
|
||||
(df_subset['Anzahl_Servicetechniker_Numeric'] > 0)
|
||||
].copy()
|
||||
filtered_rows = len(df_filtered)
|
||||
debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt aufgrund fehlender/ungültiger Technikerzahl.")
|
||||
debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}")
|
||||
|
||||
if filtered_rows == 0:
|
||||
debug_print("FEHLER: Keine Zeilen mit gültiger Technikerzahl übrig!")
|
||||
return None
|
||||
|
||||
# --- 4. Techniker-Buckets erstellen ---
|
||||
bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')] # -1 um 0 einzuschließen
|
||||
# Labels sollten keine Sonderzeichen enthalten, die Probleme machen könnten
|
||||
labels = ['Bucket_1_0', 'Bucket_2_<20', 'Bucket_3_<50', 'Bucket_4_<100', 'Bucket_5_<250', 'Bucket_6_<500', 'Bucket_7_>499']
|
||||
df_filtered['Techniker_Bucket'] = pd.cut(
|
||||
df_filtered['Anzahl_Servicetechniker_Numeric'],
|
||||
bins=bins,
|
||||
labels=labels,
|
||||
right=True # 19 gehört zu <20, 49 zu <50 etc.
|
||||
)
|
||||
debug_print("Techniker-Buckets erstellt.")
|
||||
debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts()}")
|
||||
|
||||
# --- 5. Kategoriale Features vorbereiten (Branche) ---
|
||||
branche_col = "CRM Branche" # Annahme: CRM Branche ist die zu verwendende
|
||||
debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...")
|
||||
|
||||
# Stelle sicher, dass die Spalte String ist und fülle evtl. NaNs
|
||||
df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt')
|
||||
|
||||
# One-Hot Encoding
|
||||
df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False) # dummy_na=False: keine extra Spalte für NaN
|
||||
debug_print(f"One-Hot Encoding für Branche durchgeführt. Neue Spaltenanzahl: {len(df_encoded.columns)}")
|
||||
|
||||
# --- 6. Finale Auswahl der Features für das Modell ---
|
||||
# Liste aller Feature-Spalten (One-Hot Branchen + numerische)
|
||||
feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')]
|
||||
feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter'])
|
||||
|
||||
# Zielspalte
|
||||
target_column = 'Techniker_Bucket'
|
||||
|
||||
# Erstelle den finalen DataFrame
|
||||
df_model_ready = df_encoded[feature_columns + [target_column]].copy()
|
||||
|
||||
# Optional: Spalten auf einfache Typen reduzieren (kann Speicher sparen)
|
||||
for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']:
|
||||
df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce')
|
||||
|
||||
# Reset Index für saubere Verarbeitung im nächsten Schritt
|
||||
df_model_ready = df_model_ready.reset_index(drop=True)
|
||||
|
||||
debug_print("Datenvorbereitung abgeschlossen.")
|
||||
debug_print(f"Finaler DataFrame für Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.")
|
||||
debug_print(f"Feature-Spalten: {feature_columns}")
|
||||
debug_print(f"Ziel-Spalte: {target_column}")
|
||||
|
||||
# WICHTIG: Dieser DataFrame enthält noch NaNs in 'Finaler_Umsatz'/'Finaler_Mitarbeiter'!
|
||||
# Die Imputation sollte NACH dem Train/Test Split erfolgen.
|
||||
nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum()
|
||||
debug_print(f"Fehlende Werte in numerischen Features:\n{nan_counts}")
|
||||
|
||||
return df_model_ready
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"FEHLER während der Datenvorbereitung: {e}")
|
||||
import traceback
|
||||
debug_print(traceback.format_exc())
|
||||
return None
|
||||
|
||||
# --- Beispielhafter Aufruf (zum Testen) ---
|
||||
# if __name__ == '__main__':
|
||||
# # Annahme: Config, COLUMN_MAP, debug_print sind definiert
|
||||
# # Annahme: GoogleSheetHandler existiert und verbindet sich
|
||||
# Config.load_api_keys() # Nur falls für extract_numeric_value nötig
|
||||
# LOG_FILE = create_log_filename("dataprep_test")
|
||||
# debug_print("Starte Test der Datenvorbereitung...")
|
||||
# try:
|
||||
# sheet_handler_instance = GoogleSheetHandler()
|
||||
# prepared_df = prepare_data_for_modeling(sheet_handler_instance)
|
||||
# if prepared_df is not None:
|
||||
# print("\n--- Vorbereiteter DataFrame (erste 5 Zeilen): ---")
|
||||
# print(prepared_df.head())
|
||||
# print("\n--- DataFrame Info: ---")
|
||||
# prepared_df.info()
|
||||
# print("\n--- Beschreibung numerischer Features: ---")
|
||||
# print(prepared_df[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].describe())
|
||||
# else:
|
||||
# print("Datenvorbereitung fehlgeschlagen.")
|
||||
# except Exception as e:
|
||||
# print(f"Fehler beim Testaufruf: {e}")
|
||||
|
||||
# ==================== RETRY-DECORATOR ====================
|
||||
def retry_on_failure(func):
|
||||
@@ -2507,6 +2734,201 @@ class DataProcessor:
|
||||
|
||||
debug_print(f"Modus 22 abgeschlossen. {rows_processed} Websites ergänzt.")
|
||||
|
||||
def prepare_data_for_modeling(self): # Wird zu einer Methode
|
||||
"""
|
||||
Lädt Daten aus dem Google Sheet über den sheet_handler,
|
||||
bereitet sie für das Decision Tree Modell vor:
|
||||
- Wählt relevante Spalten aus.
|
||||
- Konsolidiert Umsatz/Mitarbeiter (Wiki > CRM Priorität).
|
||||
- Filtert nach gültiger Technikerzahl (> 0).
|
||||
- Erstellt die Zielvariable (Techniker-Bucket).
|
||||
- Bereitet Features auf (One-Hot Encoding für Branche).
|
||||
- Behält NaNs in numerischen Features für spätere Imputation.
|
||||
|
||||
Returns:
|
||||
pandas.DataFrame: Vorbereiteter DataFrame für Training/Test-Split,
|
||||
oder None bei Fehlern.
|
||||
"""
|
||||
debug_print("Starte Datenvorbereitung für Modellierung...")
|
||||
|
||||
try:
|
||||
# --- 1. Daten laden & Spalten auswählen ---
|
||||
# Zugriff auf sheet_values über self.sheet_handler
|
||||
if not self.sheet_handler or not self.sheet_handler.sheet_values:
|
||||
debug_print("Fehler: Sheet Handler nicht initialisiert oder keine Daten geladen.")
|
||||
return None
|
||||
|
||||
all_data = self.sheet_handler.sheet_values # Verwende die bereits geladenen Daten
|
||||
if len(all_data) <= 5:
|
||||
debug_print("Fehler: Nicht genügend Datenzeilen im Sheet gefunden.")
|
||||
return None
|
||||
|
||||
# Annahme: Die ersten Header (Zeile 1) enthalten die Spaltennamen
|
||||
headers = all_data[0]
|
||||
data_rows = all_data[5:] # Daten ohne die ersten 5 Header-Zeilen
|
||||
|
||||
df = pd.DataFrame(data_rows, columns=headers)
|
||||
debug_print(f"DataFrame erstellt mit {len(df)} Zeilen und {len(df.columns)} Spalten.")
|
||||
|
||||
# Wähle benötigte Spalten aus ( passe die Schlüssel an deine COLUMN_MAP an!)
|
||||
# Verwende die global definierte COLUMN_MAP
|
||||
required_cols_keys_in_map = [
|
||||
"CRM Name",
|
||||
"CRM Branche",
|
||||
"CRM Umsatz",
|
||||
"Wiki Umsatz",
|
||||
"CRM Anzahl Mitarbeiter",
|
||||
"Wiki Mitarbeiter",
|
||||
"CRM Anzahl Techniker" # <-- ANPASSEN, falls die bekannte Zahl woanders steht!
|
||||
]
|
||||
|
||||
# Finde die tatsächlichen Spaltennamen
|
||||
cols_to_select = []
|
||||
missing_keys = []
|
||||
# Wir gehen davon aus, dass die *erste* Zeile (Index 0) die tatsächlichen Headernamen sind
|
||||
actual_headers = {name: idx for idx, name in enumerate(all_data[0])}
|
||||
|
||||
# Überprüfe, ob alle benötigten Spalten via COLUMN_MAP gefunden werden
|
||||
# und hole die echten Spaltennamen
|
||||
# Annahme: COLUMN_MAP mappt Beschreibung (Zeile 4?) zu Index (0-basiert)
|
||||
col_indices = {}
|
||||
try:
|
||||
tech_col_key = "CRM Anzahl Techniker" # Schlüssel in COLUMN_MAP anpassen!
|
||||
col_indices = {
|
||||
"name": all_data[0][COLUMN_MAP["CRM Name"]],
|
||||
"branche": all_data[0][COLUMN_MAP["CRM Branche"]],
|
||||
"umsatz_crm": all_data[0][COLUMN_MAP["CRM Umsatz"]],
|
||||
"umsatz_wiki": all_data[0][COLUMN_MAP["Wiki Umsatz"]],
|
||||
"ma_crm": all_data[0][COLUMN_MAP["CRM Anzahl Mitarbeiter"]],
|
||||
"ma_wiki": all_data[0][COLUMN_MAP["Wiki Mitarbeiter"]],
|
||||
"techniker": all_data[0][COLUMN_MAP[tech_col_key]]
|
||||
}
|
||||
cols_to_select = list(col_indices.values())
|
||||
except KeyError as e:
|
||||
debug_print(f"FEHLER: Konnte Mapping für Schlüssel '{e}' in COLUMN_MAP nicht finden oder Spalte nicht im Header.")
|
||||
return None
|
||||
except IndexError as e:
|
||||
debug_print(f"FEHLER: Spaltenindex aus COLUMN_MAP ist außerhalb der Grenzen der Header-Zeile: {e}")
|
||||
return None
|
||||
|
||||
|
||||
df_subset = df[cols_to_select].copy()
|
||||
# Spalten umbenennen für einfachere Handhabung intern
|
||||
rename_map = {v: k for k, v in col_indices.items()}
|
||||
df_subset.rename(columns=rename_map, inplace=True)
|
||||
|
||||
debug_print(f"Benötigte Spalten ausgewählt und umbenannt: {list(df_subset.columns)}")
|
||||
|
||||
|
||||
# --- 2. Features konsolidieren (Umsatz, Mitarbeiter) ---
|
||||
# Hilfsfunktion bleibt dieselbe
|
||||
def get_valid_numeric(value_str):
|
||||
if value_str is None or pd.isna(value_str) or value_str == '': return np.nan # Leere Strings auch als NaN
|
||||
try:
|
||||
val = float(str(value_str).replace(',', '.')) # Komma als Dezimaltrenner erlauben
|
||||
return val if val > 0 else np.nan
|
||||
except (ValueError, TypeError):
|
||||
cleaned_str = re.sub(r'[^\d.]', '', str(value_str)) # Nur Ziffern und Punkt behalten
|
||||
if not cleaned_str: return np.nan
|
||||
try:
|
||||
val = float(cleaned_str)
|
||||
return val if val > 0 else np.nan
|
||||
except ValueError:
|
||||
return np.nan
|
||||
|
||||
# Konvertiere Quellen-Spalten und wende Priorisierung an
|
||||
cols_to_process = {
|
||||
'Umsatz': ('umsatz_wiki', 'umsatz_crm', 'Finaler_Umsatz'),
|
||||
'Mitarbeiter': ('ma_wiki', 'ma_crm', 'Finaler_Mitarbeiter')
|
||||
}
|
||||
|
||||
for base_name, (wiki_col, crm_col, final_col) in cols_to_process.items():
|
||||
debug_print(f"Verarbeite '{base_name}' (Wiki: {wiki_col}, CRM: {crm_col})...")
|
||||
# Stelle sicher, dass Spalten existieren bevor apply angewendet wird
|
||||
if wiki_col not in df_subset.columns: df_subset[wiki_col] = np.nan
|
||||
if crm_col not in df_subset.columns: df_subset[crm_col] = np.nan
|
||||
|
||||
wiki_numeric = df_subset[wiki_col].apply(get_valid_numeric)
|
||||
crm_numeric = df_subset[crm_col].apply(get_valid_numeric)
|
||||
|
||||
df_subset[final_col] = np.where(
|
||||
wiki_numeric.notna(), wiki_numeric, # Wiki hat Prio 1 (wenn nicht NaN)
|
||||
np.where(crm_numeric.notna(), crm_numeric, np.nan) # Sonst CRM (wenn nicht NaN)
|
||||
)
|
||||
debug_print(f" -> {df_subset[final_col].notna().sum()} gültige '{final_col}' Werte erstellt.")
|
||||
|
||||
# --- 3. Zielvariable vorbereiten (Technikerzahl) ---
|
||||
techniker_col = "techniker" # Umbenannter Spaltenname
|
||||
debug_print(f"Verarbeite Zielvariable '{techniker_col}'...")
|
||||
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'] = pd.to_numeric(df_subset[techniker_col], errors='coerce')
|
||||
|
||||
initial_rows = len(df_subset)
|
||||
df_filtered = df_subset[
|
||||
df_subset['Anzahl_Servicetechniker_Numeric'].notna() &
|
||||
(df_subset['Anzahl_Servicetechniker_Numeric'] > 0)
|
||||
].copy()
|
||||
filtered_rows = len(df_filtered)
|
||||
debug_print(f"{initial_rows - filtered_rows} Zeilen entfernt aufgrund fehlender/ungültiger Technikerzahl.")
|
||||
debug_print(f"Verbleibende Zeilen für Modellierung: {filtered_rows}")
|
||||
|
||||
if filtered_rows < 50: # Mindestanzahl für sinnvolles Training
|
||||
debug_print(f"WARNUNG: Nur {filtered_rows} Zeilen mit gültiger Technikerzahl. Modelltraining möglicherweise nicht sinnvoll.")
|
||||
# return None # Optional hier abbrechen
|
||||
if filtered_rows == 0: return None
|
||||
|
||||
|
||||
# --- 4. Techniker-Buckets erstellen ---
|
||||
bins = [-1, 0, 19, 49, 99, 249, 499, float('inf')]
|
||||
labels = ['Bucket_1_(0)', 'Bucket_2_(<20)', 'Bucket_3_(<50)', 'Bucket_4_(<100)', 'Bucket_5_(<250)', 'Bucket_6_(<500)', 'Bucket_7_(>499)']
|
||||
df_filtered['Techniker_Bucket'] = pd.cut(
|
||||
df_filtered['Anzahl_Servicetechniker_Numeric'],
|
||||
bins=bins, labels=labels, right=True
|
||||
)
|
||||
debug_print("Techniker-Buckets erstellt.")
|
||||
debug_print(f"Verteilung der Buckets:\n{df_filtered['Techniker_Bucket'].value_counts(normalize=True).round(3)}") # Relative Häufigkeit
|
||||
|
||||
|
||||
# --- 5. Kategoriale Features vorbereiten (Branche) ---
|
||||
branche_col = "branche" # Umbenannter Spaltenname
|
||||
debug_print(f"Verarbeite kategoriales Feature '{branche_col}'...")
|
||||
|
||||
df_filtered[branche_col] = df_filtered[branche_col].astype(str).fillna('Unbekannt').str.strip() # Leerzeichen entfernen
|
||||
|
||||
# One-Hot Encoding
|
||||
df_encoded = pd.get_dummies(df_filtered, columns=[branche_col], prefix='Branche', dummy_na=False)
|
||||
debug_print(f"One-Hot Encoding für Branche durchgeführt. Neue Spaltenanzahl: {len(df_encoded.columns)}")
|
||||
|
||||
|
||||
# --- 6. Finale Auswahl der Features für das Modell ---
|
||||
feature_columns = [col for col in df_encoded.columns if col.startswith('Branche_')]
|
||||
feature_columns.extend(['Finaler_Umsatz', 'Finaler_Mitarbeiter'])
|
||||
|
||||
target_column = 'Techniker_Bucket'
|
||||
original_data_cols = ['name', 'Anzahl_Servicetechniker_Numeric'] # Behalte Original-Technikerzahl und Name für Referenz
|
||||
|
||||
df_model_ready = df_encoded[original_data_cols + feature_columns + [target_column]].copy()
|
||||
|
||||
for col in ['Finaler_Umsatz', 'Finaler_Mitarbeiter']:
|
||||
df_model_ready[col] = pd.to_numeric(df_model_ready[col], errors='coerce')
|
||||
|
||||
df_model_ready = df_model_ready.reset_index(drop=True)
|
||||
|
||||
debug_print("Datenvorbereitung abgeschlossen.")
|
||||
debug_print(f"Finaler DataFrame für Modellierung hat {len(df_model_ready)} Zeilen und {len(df_model_ready.columns)} Spalten.")
|
||||
# debug_print(f"Feature-Spalten: {feature_columns}") # Kann sehr lang sein
|
||||
debug_print(f"Ziel-Spalte: {target_column}")
|
||||
|
||||
nan_counts = df_model_ready[['Finaler_Umsatz', 'Finaler_Mitarbeiter']].isna().sum()
|
||||
debug_print(f"Fehlende Werte in numerischen Features vor Imputation:\n{nan_counts}")
|
||||
|
||||
return df_model_ready
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"FEHLER während der Datenvorbereitung: {e}")
|
||||
import traceback
|
||||
debug_print(traceback.format_exc())
|
||||
return None
|
||||
|
||||
# ==================== MAIN FUNCTION ====================
|
||||
def main():
|
||||
@@ -2515,18 +2937,20 @@ def main():
|
||||
# --- Initialisierung ---
|
||||
# Argument Parser
|
||||
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript")
|
||||
# Modus Argument
|
||||
parser.add_argument("--mode", type=str, help="Betriebsmodus (z.B. combined, wiki, website, branch, reeval, website_lookup, website_details, contacts, full_run, alignment)")
|
||||
# Limit Argument
|
||||
# HIER NEU: 'train_technician_model' als Option hinzugefügt
|
||||
parser.add_argument("--mode", type=str, help="Betriebsmodus (z.B. combined, ..., full_run, alignment, train_technician_model)")
|
||||
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen (für Batch/sequentielle Modi)", default=None)
|
||||
# Optional: Argumente speziell für das Training hinzufügen? z.B. --output_model_file
|
||||
# parser.add_argument("--model_out", type=str, default="technician_model.pkl", help="Dateipfad zum Speichern des trainierten Modells")
|
||||
# parser.add_argument("--patterns_out", type=str, default="technician_patterns.json", help="Dateipfad zum Speichern der extrahierten Muster")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Lade API Keys
|
||||
Config.load_api_keys()
|
||||
|
||||
# Betriebsmodus ermitteln
|
||||
# HIER NEU: 'alignment' hinzugefügt
|
||||
valid_modes = ["combined", "wiki", "website", "branch", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment"]
|
||||
# HIER NEU: 'train_technician_model' hinzugefügt
|
||||
valid_modes = ["combined", "wiki", "website", "branch", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model"]
|
||||
mode = None
|
||||
# Priorisiere Kommandozeilenargumente
|
||||
if args.mode and args.mode.lower() in valid_modes:
|
||||
@@ -2544,18 +2968,17 @@ def main():
|
||||
print(" website_details:Extrahiert Title/Desc/H-Tags für Zeilen mit 'x' in Spalte A")
|
||||
print(" contacts: Sucht LinkedIn Kontakte via SERP API und schreibt in 'Contacts' Blatt")
|
||||
print(" full_run: Verarbeitet alle Zeilen sequentiell ab der ersten ohne Zeitstempel (AO)")
|
||||
print(" alignment: Schreibt die Definitions-Header (Zeilen 1-5) ins Hauptblatt (Überschreibt A1:AS5!)") # NEUE Beschreibung
|
||||
print(" alignment: Schreibt die Definitions-Header (Zeilen 1-5) ins Hauptblatt (Überschreibt A1:AS5!)")
|
||||
print(" train_technician_model: Bereitet Daten vor, trainiert & evaluiert Decision Tree zur Technikerschätzung") # NEUE Beschreibung
|
||||
try:
|
||||
# HIER NEU: valid_modes für die Hilfsanzeige verwendet
|
||||
mode_input = input(f"Geben Sie den Modus ein ({', '.join(valid_modes)}): ").strip().lower()
|
||||
if mode_input in valid_modes:
|
||||
mode = mode_input
|
||||
else:
|
||||
print("Ungültige Eingabe. Standardmodus 'combined' wird verwendet.")
|
||||
mode = "combined" # Standardmodus
|
||||
# Fehlerbehandlung für input() im Hintergrund
|
||||
mode = "combined"
|
||||
except OSError as e:
|
||||
if e.errno == 9: # Bad file descriptor
|
||||
if e.errno == 9:
|
||||
print("Fehler: Interaktive Modus-Abfrage nicht möglich (läuft im Hintergrund?). Standardmodus 'combined' wird verwendet.")
|
||||
mode = "combined"
|
||||
else:
|
||||
@@ -2568,6 +2991,8 @@ def main():
|
||||
|
||||
|
||||
# Zeilenlimit ermitteln (Logik bleibt unverändert, fragt nur wenn nötig)
|
||||
# Hinweis: Das Limit wird im 'train_technician_model' Modus aktuell nicht direkt verwendet,
|
||||
# da alle verfügbaren Daten mit Technikerzahl genutzt werden sollten.
|
||||
row_limit = None
|
||||
if args.limit is not None:
|
||||
if args.limit >= 0:
|
||||
@@ -2576,7 +3001,7 @@ def main():
|
||||
else:
|
||||
print("Warnung: Negatives Zeilenlimit ignoriert. Kein Limit gesetzt.")
|
||||
row_limit = None
|
||||
elif mode in ["combined", "wiki", "website", "branch", "full_run"]: # Limit nur für diese Modi relevant
|
||||
elif mode in ["combined", "wiki", "website", "branch", "full_run"]: # Limit nur für diese Modi interaktiv abfragen
|
||||
try:
|
||||
limit_input = input("Wie viele Zeilen sollen maximal bearbeitet werden? (Enter für alle) ")
|
||||
if limit_input.strip():
|
||||
@@ -2611,15 +3036,16 @@ def main():
|
||||
debug_print(f"===== Skript gestartet =====")
|
||||
debug_print(f"Version: {Config.VERSION}")
|
||||
debug_print(f"Betriebsmodus: {mode}")
|
||||
# Korrigiere Logging für row_limit, wenn es 0 ist
|
||||
limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt'
|
||||
if row_limit == 0 and mode in ["combined", "wiki", "website", "branch", "full_run"]:
|
||||
limit_log_text = '0 (Keine Verarbeitung geplant)'
|
||||
limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus'
|
||||
if mode in ["combined", "wiki", "website", "branch", "full_run"]:
|
||||
limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt'
|
||||
if row_limit == 0:
|
||||
limit_log_text = '0 (Keine Verarbeitung geplant)'
|
||||
debug_print(f"Zeilenlimit: {limit_log_text}")
|
||||
debug_print(f"Logdatei: {LOG_FILE}")
|
||||
|
||||
# --- Vorbereitung ---
|
||||
# Lade Branchenschema
|
||||
# Lade Branchenschema (wird für fast alle Modi benötigt)
|
||||
load_target_schema()
|
||||
|
||||
# Initialisiere Google Sheet Handler
|
||||
@@ -2627,10 +3053,10 @@ def main():
|
||||
sheet_handler = GoogleSheetHandler()
|
||||
except Exception as e:
|
||||
debug_print(f"FATAL: Konnte Google Sheet Handler nicht initialisieren: {e}")
|
||||
print(f"FEHLER: Verbindung zu Google Sheets fehlgeschlagen. Siehe Logdatei: {LOG_FILE}") # Direkte Ausgabe für den User
|
||||
print(f"FEHLER: Verbindung zu Google Sheets fehlgeschlagen. Siehe Logdatei: {LOG_FILE}")
|
||||
return # Abbruch
|
||||
|
||||
# Initialisiere DataProcessor
|
||||
# Initialisiere DataProcessor (wird für einige Modi gebraucht)
|
||||
data_processor = DataProcessor(sheet_handler)
|
||||
|
||||
# --- Modusausführung ---
|
||||
@@ -2639,7 +3065,6 @@ def main():
|
||||
|
||||
try:
|
||||
if mode in ["wiki", "website", "branch", "combined"]:
|
||||
# Stelle sicher, dass row_limit nicht 0 ist, sonst startet der Dispatcher umsonst
|
||||
if row_limit == 0:
|
||||
debug_print("Zeilenlimit ist 0. Überspringe Dispatcher-Aufruf.")
|
||||
else:
|
||||
@@ -2653,7 +3078,6 @@ def main():
|
||||
elif mode == "contacts":
|
||||
process_contact_research(sheet_handler)
|
||||
elif mode == "full_run":
|
||||
# Behandlung von Limit 0 direkt hier
|
||||
if row_limit == 0:
|
||||
debug_print("Zeilenlimit ist 0. Überspringe sequenzielle Verarbeitung.")
|
||||
else:
|
||||
@@ -2671,7 +3095,6 @@ def main():
|
||||
debug_print("Keine Zeilen für 'full_run' zu verarbeiten (Limit 0 oder Startindex am Ende).")
|
||||
else:
|
||||
debug_print(f"Startindex {start_index} liegt hinter der letzten Datenzeile. Keine Verarbeitung für 'full_run'.")
|
||||
# HIER NEU: elif für den alignment Modus
|
||||
elif mode == "alignment":
|
||||
print("\nACHTUNG: Dieser Modus überschreibt die Zellen A1:AS5 im Haupt-Sheet!")
|
||||
print("Diese Zellen enthalten die Spaltendefinitionen (Alignment Demo).")
|
||||
@@ -2679,14 +3102,13 @@ def main():
|
||||
confirm = input("Möchten Sie wirklich fortfahren? (j/N): ").strip().lower()
|
||||
if confirm == 'j':
|
||||
debug_print("Bestätigung erhalten. Starte Alignment Demo...")
|
||||
alignment_demo(sheet_handler.sheet) # Rufe die Funktion auf
|
||||
alignment_demo(sheet_handler.sheet)
|
||||
debug_print("Alignment Demo Aufruf beendet.")
|
||||
else:
|
||||
print("Vorgang abgebrochen.")
|
||||
debug_print("Alignment Demo vom Benutzer abgebrochen.")
|
||||
# Fehlerbehandlung für input() im Hintergrund
|
||||
except OSError as e:
|
||||
if e.errno == 9: # Bad file descriptor
|
||||
if e.errno == 9:
|
||||
print("Fehler: Interaktive Bestätigung nicht möglich (läuft im Hintergrund?). Vorgang abgebrochen.")
|
||||
debug_print("Alignment Demo abgebrochen (keine interaktive Bestätigung möglich).")
|
||||
else:
|
||||
@@ -2696,6 +3118,134 @@ def main():
|
||||
print("Fehler: Interaktive Bestätigung nicht möglich (EOF). Vorgang abgebrochen.")
|
||||
debug_print("Alignment Demo abgebrochen (EOF).")
|
||||
|
||||
# HIER NEU: Block für den Modelltrainings-Modus
|
||||
elif mode == "train_technician_model":
|
||||
debug_print("Starte Modus: train_technician_model")
|
||||
|
||||
# 1. Daten vorbereiten
|
||||
prepared_df = prepare_data_for_modeling(sheet_handler)
|
||||
|
||||
if prepared_df is not None and not prepared_df.empty:
|
||||
# 2. Train/Test Split
|
||||
debug_print("Aufteilen der Daten in Trainings- und Testsets...")
|
||||
try:
|
||||
X = prepared_df.drop(columns=['Techniker_Bucket'])
|
||||
y = prepared_df['Techniker_Bucket']
|
||||
# Stratify=y ist wichtig, um die Verteilung der Buckets in Train/Test ähnlich zu halten
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.25, random_state=42, stratify=y
|
||||
)
|
||||
debug_print(f"Trainingsdaten: {X_train.shape}, Testdaten: {X_test.shape}")
|
||||
except Exception as e:
|
||||
debug_print(f"Fehler beim Train/Test Split: {e}")
|
||||
X_train, X_test, y_train, y_test = None, None, None, None # Zurücksetzen
|
||||
|
||||
if X_train is not None:
|
||||
# 3. Imputation fehlender Werte (Umsatz/Mitarbeiter)
|
||||
debug_print("Imputation fehlender Werte (Median)...")
|
||||
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
|
||||
try:
|
||||
imputer = SimpleImputer(strategy='median')
|
||||
# WICHTIG: Imputer NUR auf Trainingsdaten fitten!
|
||||
imputer.fit(X_train[numeric_features])
|
||||
# Transformiere Trainings- UND Testdaten
|
||||
X_train_imputed_np = imputer.transform(X_train[numeric_features])
|
||||
X_test_imputed_np = imputer.transform(X_test[numeric_features])
|
||||
|
||||
# Konvertiere zurück zu DataFrames und setze Spaltennamen und Index zurück
|
||||
X_train[numeric_features] = X_train_imputed_np
|
||||
X_test[numeric_features] = X_test_imputed_np
|
||||
|
||||
# Speichere den Imputer für spätere Verwendung (z.B. bei neuen Daten)
|
||||
imputer_filename = "median_imputer.pkl"
|
||||
with open(imputer_filename, 'wb') as f_imputer:
|
||||
pickle.dump(imputer, f_imputer)
|
||||
debug_print(f"Median-Imputer trainiert und gespeichert als '{imputer_filename}'.")
|
||||
imputation_successful = True
|
||||
except Exception as e:
|
||||
debug_print(f"Fehler bei der Imputation: {e}")
|
||||
imputation_successful = False
|
||||
|
||||
if imputation_successful:
|
||||
# 4. Modelltraining & Hyperparameter-Tuning (Beispielhaft)
|
||||
debug_print("Starte Decision Tree Training mit GridSearchCV...")
|
||||
# Definiere den Parameter-Grid für die Suche
|
||||
param_grid = {
|
||||
'criterion': ['gini', 'entropy'],
|
||||
'max_depth': [5, 8, 10, 12, None], # None = unbegrenzt (vorsicht)
|
||||
'min_samples_split': [10, 20, 40],
|
||||
'min_samples_leaf': [5, 10, 20],
|
||||
'ccp_alpha': [0.0, 0.001, 0.005, 0.01] # Für Pruning
|
||||
}
|
||||
|
||||
# Erstelle Decision Tree Classifier
|
||||
dtree = DecisionTreeClassifier(random_state=42)
|
||||
|
||||
# Erstelle GridSearchCV Objekt (cv=5 für 5-fache Kreuzvalidierung)
|
||||
# scoring='accuracy' oder 'f1_weighted' etc.
|
||||
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='accuracy', n_jobs=-1, verbose=1) # n_jobs=-1 nutzt alle CPU Kerne
|
||||
|
||||
try:
|
||||
grid_search.fit(X_train, y_train)
|
||||
|
||||
# Bestes Modell und Parameter ausgeben
|
||||
best_params = grid_search.best_params_
|
||||
best_score = grid_search.best_score_
|
||||
best_estimator = grid_search.best_estimator_
|
||||
debug_print(f"GridSearchCV abgeschlossen.")
|
||||
debug_print(f"Beste Parameter gefunden: {best_params}")
|
||||
debug_print(f"Bester Kreuzvalidierungs-Score (Accuracy): {best_score:.4f}")
|
||||
|
||||
# Speichere das beste Modell
|
||||
model_filename = "technician_decision_tree_model.pkl"
|
||||
with open(model_filename, 'wb') as f_model:
|
||||
pickle.dump(best_estimator, f_model)
|
||||
debug_print(f"Bestes Modell gespeichert als '{model_filename}'.")
|
||||
|
||||
# 5. Evaluation auf dem Test-Set
|
||||
debug_print("Evaluiere bestes Modell auf dem Test-Set...")
|
||||
y_pred = best_estimator.predict(X_test)
|
||||
|
||||
test_accuracy = accuracy_score(y_test, y_pred)
|
||||
report = classification_report(y_test, y_pred, zero_division=0)
|
||||
conf_matrix = confusion_matrix(y_test, y_pred)
|
||||
|
||||
debug_print(f"\n--- Evaluationsergebnisse (Test-Set) ---")
|
||||
debug_print(f"Genauigkeit: {test_accuracy:.4f}")
|
||||
debug_print(f"Klassifikationsbericht:\n{report}")
|
||||
debug_print(f"Konfusionsmatrix:\n{conf_matrix}")
|
||||
print(f"\nModell-Evaluation abgeschlossen. Genauigkeit auf Test-Set: {test_accuracy:.4f}") # Auch für User sichtbar
|
||||
print(f"Detaillierter Bericht im Logfile: {LOG_FILE}")
|
||||
|
||||
# 6. Muster extrahieren
|
||||
debug_print("\nExtrahiere Regeln aus dem besten Baum (Textformat)...")
|
||||
try:
|
||||
feature_names = list(X_train.columns) # Namen der Features
|
||||
# Stelle sicher, dass die Label-Namen (Buckets) verfügbar sind
|
||||
class_names = best_estimator.classes_ # Die Bucket-Labels
|
||||
|
||||
rules_text = export_text(best_estimator, feature_names=feature_names, show_weights=True) # show_weights zeigt Verteilung in Blättern
|
||||
debug_print(f"--- Baumregeln (Text) ---:\n{rules_text}")
|
||||
|
||||
# Speichere Regeln als Textdatei
|
||||
patterns_filename_txt = "technician_patterns.txt"
|
||||
with open(patterns_filename_txt, 'w', encoding='utf-8') as f_rules:
|
||||
f_rules.write(rules_text)
|
||||
debug_print(f"Regeln als Text gespeichert in '{patterns_filename_txt}'.")
|
||||
|
||||
# TODO (Optional): Regeln als JSON extrahieren (komplexer)
|
||||
# Hier müsste man den Baum traversieren (tree_ Attribut)
|
||||
|
||||
except Exception as e_export:
|
||||
debug_print(f"Fehler beim Extrahieren/Speichern der Baumregeln: {e_export}")
|
||||
|
||||
except Exception as e_train:
|
||||
debug_print(f"FEHLER während des Modelltrainings/-tunings: {e_train}")
|
||||
import traceback
|
||||
debug_print(traceback.format_exc())
|
||||
else:
|
||||
debug_print("Datenvorbereitung fehlgeschlagen oder keine Daten vorhanden. Modus 'train_technician_model' abgebrochen.")
|
||||
|
||||
else:
|
||||
debug_print(f"Unbekannter Modus '{mode}' - keine Aktion ausgeführt.")
|
||||
|
||||
@@ -2710,7 +3260,6 @@ def main():
|
||||
debug_print(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.")
|
||||
debug_print(f"Gesamtdauer: {duration:.2f} Sekunden.")
|
||||
debug_print(f"===== Skript beendet =====")
|
||||
# Sicherstellen, dass die letzte Log-Nachricht auch geschrieben wird
|
||||
if LOG_FILE:
|
||||
try:
|
||||
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||
|
||||
Reference in New Issue
Block a user