This commit is contained in:
2025-04-17 14:36:23 +00:00
parent 5c00505dff
commit 4c2ef0d251

View File

@@ -573,6 +573,161 @@ def normalize_company_name(name):
return normalized.lower()
# NEUE Funktion für Wiki-Updates basierend auf ChatGPT Vorschlägen
def process_wiki_updates_from_chatgpt(sheet_handler, data_processor):
"""
Identifiziert Zeilen, bei denen ChatGPT einen alternativen Wiki-Artikel vorgeschlagen hat (S='X', T=URL),
kopiert die neue URL nach M, führt ein Reparse der Wiki-Daten (N-R) durch,
berechnet die Branche neu (W-Y) und löscht relevante Timestamps (AN, AX, AO, AP).
"""
debug_print("Starte Modus: Wiki-Updates basierend auf ChatGPT-Vorschlägen...")
if not sheet_handler.load_data(): return
all_data = sheet_handler.get_all_data_with_headers()
if not all_data or len(all_data) <= 5: return
header_rows = 5
data_rows = all_data[header_rows:] # Arbeite mit Daten ohne Header
# Indizes holen (Beispielhaft, passe Schlüssel an deine COLUMN_MAP an)
wiki_konsistenz_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") # S
vorschlag_t_idx = COLUMN_MAP.get("Chat Vorschlag Wiki Artikel") # T
wiki_url_m_idx = COLUMN_MAP.get("Wiki URL") # M
# Indizes für Wiki-Daten N-R
absatz_n_idx = COLUMN_MAP.get("Wiki Absatz")
branche_o_idx = COLUMN_MAP.get("Wiki Branche")
umsatz_p_idx = COLUMN_MAP.get("Wiki Umsatz")
ma_q_idx = COLUMN_MAP.get("Wiki Mitarbeiter")
kat_r_idx = COLUMN_MAP.get("Wiki Kategorien")
# Indizes für Branch W-Y
branch_w_idx = COLUMN_MAP.get("Chat Vorschlag Branche")
branch_x_idx = COLUMN_MAP.get("Chat Konsistenz Branche")
branch_y_idx = COLUMN_MAP.get("Chat Begründung Abweichung Branche")
# Indizes für Timestamps und Version
ts_an_idx = COLUMN_MAP.get("Wikipedia Timestamp")
ts_ax_idx = COLUMN_MAP.get("Wiki Verif. Timestamp")
ts_ao_idx = COLUMN_MAP.get("Timestamp letzte Prüfung")
version_ap_idx = COLUMN_MAP.get("Version")
# Index für Website Summary AS (wird für Branch-Neuberechnung gebraucht)
summary_as_idx = COLUMN_MAP.get("Website Zusammenfassung")
# Index für CRM Branche/Beschreibung (für Branch-Neuberechnung)
crm_branch_idx = COLUMN_MAP.get("CRM Branche")
crm_desc_idx = COLUMN_MAP.get("CRM Beschreibung")
# Prüfe, ob alle Indizes gefunden wurden
required_indices = [
wiki_konsistenz_idx, vorschlag_t_idx, wiki_url_m_idx, absatz_n_idx, branche_o_idx,
umsatz_p_idx, ma_q_idx, kat_r_idx, branch_w_idx, branch_x_idx, branch_y_idx,
ts_an_idx, ts_ax_idx, ts_ao_idx, version_ap_idx, summary_as_idx,
crm_branch_idx, crm_desc_idx
]
if None in required_indices:
missing = [k for k, v in COLUMN_MAP.items() if v in required_indices and v is None] # Finde fehlende Keys
debug_print(f"FEHLER: Mindestens ein benötigter Spaltenindex für Wiki-Updates fehlt in COLUMN_MAP. Fehlende Keys (Beispiel): {missing}")
return
all_sheet_updates = []
processed_rows = 0
wiki_scraper = data_processor.wiki_scraper # Nutze den Scraper aus dem DataProcessor
# Gehe durch alle *Daten*-Zeilen
for idx, row in enumerate(data_rows):
row_num_in_sheet = idx + header_rows + 1 # 1-basierte Zeilennummer
# Lese Werte sicher aus
konsistenz_s = row[wiki_konsistenz_idx] if len(row) > wiki_konsistenz_idx else ""
vorschlag_t = row[vorschlag_t_idx] if len(row) > vorschlag_t_idx else ""
url_m = row[wiki_url_m_idx] if len(row) > wiki_url_m_idx else ""
# Bedingung prüfen: S='X' und T ist eine URL und T != M
is_update_candidate = False
new_url = ""
if konsistenz_s.strip().upper() == "X":
# Prüfe, ob T eine valide URL ist (einfache Prüfung)
if vorschlag_t.strip().lower().startswith(("http://", "https://")):
new_url = vorschlag_t.strip()
# Prüfe, ob die neue URL anders ist als die alte
if new_url != url_m.strip():
is_update_candidate = True
if is_update_candidate:
debug_print(f"Zeile {row_num_in_sheet}: Update-Kandidat gefunden. Neue URL: {new_url}")
processed_rows += 1
# --- Schritt 3a: Wiki Reparse ---
debug_print(f" -> Reparsing Wiki-Daten für {new_url}...")
new_wiki_data = wiki_scraper.extract_company_data(new_url)
# --- Schritt 3b: Branch Neuberechnung ---
# Hole benötigte Daten für Branch-Eval
crm_branche = row[crm_branch_idx] if len(row) > crm_branch_idx else ""
crm_beschreibung = row[crm_desc_idx] if len(row) > crm_desc_idx else ""
website_summary = row[summary_as_idx] if len(row) > summary_as_idx else ""
debug_print(f" -> Neuberechnung der Branche...")
new_branch_result = evaluate_branche_chatgpt(
crm_branche,
crm_beschreibung,
new_wiki_data.get('branche', 'k.A.'), # Neue Wiki-Branche
new_wiki_data.get('categories', 'k.A.'), # Neue Wiki-Kategorien
website_summary # Vorhandene Website-Zusammenfassung
)
# --- Schritt 4: Updates sammeln ---
# Spaltenbuchstaben ermitteln
url_m_letter = sheet_handler._get_col_letter(wiki_url_m_idx + 1)
absatz_n_letter = sheet_handler._get_col_letter(absatz_n_idx + 1)
branche_o_letter = sheet_handler._get_col_letter(branche_o_idx + 1)
umsatz_p_letter = sheet_handler._get_col_letter(umsatz_p_idx + 1)
ma_q_letter = sheet_handler._get_col_letter(ma_q_idx + 1)
kat_r_letter = sheet_handler._get_col_letter(kat_r_idx + 1)
branch_w_letter = sheet_handler._get_col_letter(branch_w_idx + 1)
branch_x_letter = sheet_handler._get_col_letter(branch_x_idx + 1)
branch_y_letter = sheet_handler._get_col_letter(branch_y_idx + 1)
ts_an_letter = sheet_handler._get_col_letter(ts_an_idx + 1)
ts_ax_letter = sheet_handler._get_col_letter(ts_ax_idx + 1)
ts_ao_letter = sheet_handler._get_col_letter(ts_ao_idx + 1)
version_ap_letter = sheet_handler._get_col_letter(version_ap_idx + 1)
# Optional: Spalte T leeren/markieren
vorschlag_t_letter = sheet_handler._get_col_letter(vorschlag_t_idx + 1)
row_updates = [
# Wiki-Daten aktualisieren
{'range': f'{url_m_letter}{row_num_in_sheet}', 'values': [[new_url]]}, # Neue URL nach M
{'range': f'{absatz_n_letter}{row_num_in_sheet}', 'values': [[new_wiki_data.get('first_paragraph', 'k.A.')]]},
{'range': f'{branche_o_letter}{row_num_in_sheet}', 'values': [[new_wiki_data.get('branche', 'k.A.')]]},
{'range': f'{umsatz_p_letter}{row_num_in_sheet}', 'values': [[new_wiki_data.get('umsatz', 'k.A.')]]},
{'range': f'{ma_q_letter}{row_num_in_sheet}', 'values': [[new_wiki_data.get('mitarbeiter', 'k.A.')]]},
{'range': f'{kat_r_letter}{row_num_in_sheet}', 'values': [[new_wiki_data.get('categories', 'k.A.')]]},
# Branch-Daten aktualisieren
{'range': f'{branch_w_letter}{row_num_in_sheet}', 'values': [[new_branch_result.get("branch", "Fehler")]]},
{'range': f'{branch_x_letter}{row_num_in_sheet}', 'values': [[new_branch_result.get("consistency", "Fehler")]]},
{'range': f'{branch_y_letter}{row_num_in_sheet}', 'values': [[new_branch_result.get("justification", "Fehler")]]},
# Timestamps und Version leeren, um erneute Prüfung zu triggern
{'range': f'{ts_an_letter}{row_num_in_sheet}', 'values': [[""]]}, # Wiki Extraktion TS
{'range': f'{ts_ax_letter}{row_num_in_sheet}', 'values': [[""]]}, # Wiki Verif. TS
{'range': f'{ts_ao_letter}{row_num_in_sheet}', 'values': [[""]]}, # Letzte Prüfung TS
{'range': f'{version_ap_letter}{row_num_in_sheet}', 'values': [[""]]}, # Version
# Optional: Spalte T leeren oder markieren
{'range': f'{vorschlag_t_letter}{row_num_in_sheet}', 'values': [["Korrektur übernommen"]]},
]
all_sheet_updates.extend(row_updates)
# Kurze Pause nach jeder Zeile, um APIs nicht zu überlasten
time.sleep(Config.RETRY_DELAY) # Pause nach Wiki Reparse + Branch Neuberechnung
# --- Schritt 5: Batch Update ---
if all_sheet_updates:
debug_print(f"Sende Batch-Update für {processed_rows} korrigierte Wiki-Einträge ({len(all_sheet_updates)} Zellen)...")
success = sheet_handler.batch_update_cells(all_sheet_updates)
if success:
debug_print(f"Sheet-Update für Wiki-Korrekturen erfolgreich.")
else:
debug_print(f"FEHLER beim Sheet-Update für Wiki-Korrekturen.")
else:
debug_print("Keine Zeilen gefunden, die eine Wiki-URL-Korrektur benötigen.")
debug_print(f"Wiki-Updates basierend auf ChatGPT abgeschlossen. {processed_rows} Zeilen aktualisiert.")
def extract_numeric_value(raw_value, is_umsatz=False):
"""Extrahiert und normalisiert Zahlenwerte (Umsatz in Mio, Mitarbeiter)."""
@@ -3579,48 +3734,71 @@ def main():
# --- Initialisierung ---
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript")
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", "website_lookup", "website_details", "contacts", "full_run", "alignment", "train_technician_model"]
# NEU: 'update_wiki' hinzugefügt
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval",
"website_lookup", "website_details", "contacts", "full_run",
"alignment", "train_technician_model", "update_wiki"]
parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})")
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen", default=None)
# --- NEU: Argumente für Modelltraining ---
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Dateipfad zum Speichern des Modells (Standard: {MODEL_FILE})")
parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Dateipfad zum Speichern des Imputers (Standard: {IMPUTER_FILE})")
parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Dateipfad zum Speichern der Text-Regeln (Standard: {PATTERNS_FILE_TXT})")
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad für Modell (.pkl)")
parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Pfad für Imputer (.pkl)")
parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)")
args = parser.parse_args()
Config.load_api_keys()
# Betriebsmodus ermitteln (wie gehabt)
# Betriebsmodus ermitteln
mode = None
if args.mode and args.mode.lower() in valid_modes: mode = args.mode.lower(); print(f"Betriebsmodus (aus Kommandozeile): {mode}")
else: # Interaktive Abfrage (wie gehabt)
# ... (print Optionen etc.) ...
try: mode_input = input(f"Geben Sie den Modus ein: ").strip().lower();
else: # Interaktive Abfrage
print("Bitte wählen Sie den Betriebsmodus:")
print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)")
print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)")
print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)")
print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)")
print(" update_wiki: Wiki-URL aus Spalte T übernehmen & Reparse/Re-Branch") # NEU
print(" reeval: Verarbeitet Zeilen mit 'x' (volle Verarbeitung, alle TS prüfen)")
print(" website_lookup: Sucht fehlende Websites (D)")
print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR)")
print(" contacts: Sucht LinkedIn Kontakte (AM)")
print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)")
print(" alignment: Schreibt Header A1:AX5 (!)")
print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung")
try:
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 -> combined"); mode = "combined"
except Exception as e: print(f"Fehler Modus-Eingabe ({e}) -> combined"); mode = "combined"
if mode_input in valid_modes: mode = mode_input
else: print("Ungültige Eingabe -> combined"); mode = "combined"
# Zeilenlimit ermitteln (wie gehabt)
# Zeilenlimit ermitteln
row_limit = None
if args.limit is not None:
if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
else: print("Warnung: Negatives Limit ignoriert."); row_limit = None
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]:
try: limit_input = input("Max Zeilen? (Enter=alle): ");
except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None
if limit_input.strip():
try: limit_val = int(limit_input);
except ValueError: print("Ungültige Zahl -> Kein Limit"); row_limit = None
if limit_val >= 0: row_limit = limit_val; print(f"Zeilenlimit: {row_limit}")
else: print("Negatives Limit -> Kein Limit"); row_limit = None
else: row_limit = None; print("Kein Zeilenlimit.")
if args.limit >= 0: row_limit = args.limit; print(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
else: print("Warnung: Negatives Limit ignoriert."); row_limit = None
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run"]: # Nur für relevante Modi fragen
try:
limit_input = input("Max Zeilen? (Enter=alle): ");
if limit_input.strip():
try:
limit_val = int(limit_input)
if limit_val >= 0: row_limit = limit_val; print(f"Zeilenlimit: {row_limit}")
else: print("Negatives Limit -> Kein Limit"); row_limit = None
except ValueError: print("Ungültige Zahl -> Kein Limit"); row_limit = None
else: row_limit = None; print("Kein Zeilenlimit.")
except Exception as e: print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit"); row_limit = None
# Logfile initialisieren (wie gehabt)
# Logfile initialisieren
LOG_FILE = create_log_filename(mode)
# ... (Logging Startparameter) ...
debug_print(f"===== Skript gestartet ====="); debug_print(f"Version: {Config.VERSION}")
debug_print(f"Betriebsmodus: {mode}"); # ... (Restliches Logging)
debug_print(f"Betriebsmodus: {mode}");
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", "summarize", "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 ---
load_target_schema()
@@ -3632,139 +3810,96 @@ def main():
start_time = time.time()
debug_print(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...")
try:
# Batch-Modi über Dispatcher
if mode in ["wiki", "website", "branch", "summarize", "combined"]:
if row_limit == 0: debug_print("Limit 0 -> Skip Dispatcher.")
else: run_dispatcher(mode, sheet_handler, row_limit)
# Einzelne Zeilen Modi (kein Batch-Dispatcher)
elif mode == "reeval": data_processor.process_reevaluation_rows()
elif mode == "website_lookup": data_processor.process_serp_website_lookup_for_empty()
elif mode == "website_details": data_processor.process_website_details_for_marked_rows()
elif mode == "contacts": process_contact_research(sheet_handler)
elif mode == "full_run":
if row_limit == 0: debug_print("Limit 0 -> Skip full_run.")
else: # Logik für full_run (wie gehabt)
start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung")
if start_index != -1 and start_index < len(sheet_handler.get_data()):
#... (Berechne num_to_process) ...
num_available = len(sheet_handler.get_data()) - start_index
num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available
if num_to_process > 0: data_processor.process_rows_sequentially(start_index, num_to_process)
else: debug_print("Keine Zeilen für full_run.")
else: debug_print(f"Startindex {start_index} für full_run ungültig.")
elif mode == "alignment": # Logik für Alignment (wie gehabt)
# ... (Bestätigungsabfrage und Aufruf) ...
print("\nACHTUNG: Überschreibt A1:AX5!");
try: confirm = input("Fortfahren? (j/N): ").strip().lower()
except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n'
if confirm == 'j': alignment_demo(sheet_handler.sheet)
else: print("Abgebrochen.")
else:
start_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung")
if start_index != -1 and start_index < len(sheet_handler.get_data()):
num_available = len(sheet_handler.get_data()) - start_index
num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available
if num_to_process > 0:
# Übergebe Flags an process_rows_sequentially
data_processor.process_rows_sequentially(start_index, num_to_process, process_wiki=True, process_chatgpt=True, process_website=True)
else: debug_print("Keine Zeilen für 'full_run' zu verarbeiten.")
else: debug_print(f"Startindex {start_index} für 'full_run' ungültig.")
elif mode == "alignment":
print("\nACHTUNG: Überschreibt A1:AX5!"); # AX statt AS
try: confirm = input("Fortfahren? (j/N): ").strip().lower()
except Exception as e_input: print(f"Input-Fehler: {e_input}"); confirm = 'n'
if confirm == 'j': alignment_demo(sheet_handler.sheet)
else: print("Abgebrochen.")
# --- NEUER BLOCK: Modelltraining ---
# --- NEU: Wiki Update Modus ---
elif mode == "update_wiki":
process_wiki_updates_from_chatgpt(sheet_handler, data_processor)
# --- Ende Wiki Update Modus ---
# Block für Modelltraining (wie von dir bereitgestellt)
elif mode == "train_technician_model":
debug_print(f"Starte Modus: {mode}")
# 1. Daten vorbereiten
prepared_df = data_processor.prepare_data_for_modeling()
if prepared_df is None or prepared_df.empty:
debug_print("FEHLER: Datenvorbereitung fehlgeschlagen oder keine Daten vorhanden. Modus wird abgebrochen.")
else:
# 2. Train/Test Split
debug_print("Aufteilen der Daten in Trainings- und Testsets (Testgröße 25%, stratifiziert)...")
if prepared_df is not None and not prepared_df.empty:
debug_print("Aufteilen der Daten...")
try:
# Features X (ohne Ziel und Hilfsspalten), Target y
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric'])
y = prepared_df['Techniker_Bucket']
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[0]} Zeilen, {X_train.shape[1]} Features")
debug_print(f"Testdaten: {X_test.shape[0]} Zeilen, {X_test.shape[1]} Features")
split_successful = True
except Exception as e:
debug_print(f"FEHLER beim Train/Test Split: {e}"); split_successful = False
except Exception as e: debug_print(f"FEHLER Split: {e}"); split_successful = False
if split_successful:
# 3. Imputation fehlender Werte
debug_print("Imputation fehlender Werte für 'Finaler_Umsatz', 'Finaler_Mitarbeiter' (Median)...")
debug_print("Imputation...")
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
try:
imputer = SimpleImputer(strategy='median')
# WICHTIG: Imputer NUR auf Trainingsdaten fitten!
X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features])
# Testdaten NUR transformieren
X_test[numeric_features] = imputer.transform(X_test[numeric_features])
# Speichere den Imputer
imputer_filename = args.imputer_out # Verwende Argument oder Default
with open(imputer_filename, 'wb') as f: pickle.dump(imputer, f)
debug_print(f"Median-Imputer trainiert und gespeichert als '{imputer_filename}'.")
imputer_filename = args.imputer_out; pickle.dump(imputer, open(imputer_filename, 'wb'))
debug_print(f"Imputer gespeichert: '{imputer_filename}'.")
imputation_successful = True
except Exception as e:
debug_print(f"FEHLER bei der Imputation: {e}"); imputation_successful = False
except Exception as e: debug_print(f"FEHLER Imputation: {e}"); imputation_successful = False
if imputation_successful:
# 4. Modelltraining & Hyperparameter-Tuning
debug_print("Starte Decision Tree Training mit GridSearchCV...")
# Verkleinerter Grid zum Testen, kann erweitert werden
param_grid = {
'criterion': ['gini', 'entropy'],
'max_depth': [6, 8, 10, 12],
'min_samples_split': [20, 40],
'min_samples_leaf': [10, 20],
'ccp_alpha': [0.0, 0.001, 0.005]
}
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') # Balanced für ungleiche Klassen?
# F1-Score oft besser für unbalancierte Klassen als Accuracy
debug_print("Starte Training/GridSearchCV...")
param_grid = { 'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15], 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30], 'ccp_alpha': [0.0, 0.001, 0.005]}
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced')
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, scoring='f1_weighted', n_jobs=-1, verbose=1)
try:
grid_search.fit(X_train, y_train)
best_estimator = grid_search.best_estimator_
debug_print(f"GridSearchCV abgeschlossen.")
debug_print(f"Beste Parameter: {grid_search.best_params_}")
debug_print(f"Bester Kreuzvalidierungs-Score (F1 Weighted): {grid_search.best_score_:.4f}")
# Speichere das beste Modell
model_filename = args.model_out # Verwende Argument oder Default
with open(model_filename, 'wb') as f: pickle.dump(best_estimator, f)
debug_print(f"Bestes Modell gespeichert als '{model_filename}'.")
debug_print(f"GridSearchCV fertig. Beste Params: {grid_search.best_params_}, Score: {grid_search.best_score_:.4f}")
model_filename = args.model_out; pickle.dump(best_estimator, open(model_filename, 'wb'))
debug_print(f"Modell gespeichert: '{model_filename}'.")
training_successful = True
except Exception as e_train:
debug_print(f"FEHLER während Training/Tuning: {e_train}"); training_successful = False
import traceback; debug_print(traceback.format_exc())
except Exception as e_train: debug_print(f"FEHLER Training: {e_train}"); training_successful = False; import traceback; debug_print(traceback.format_exc())
if training_successful:
# 5. Evaluation auf dem Test-Set
debug_print("Evaluiere bestes Modell auf dem Test-Set...")
y_pred = best_estimator.predict(X_test)
debug_print("Evaluiere 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, labels=best_estimator.classes_, target_names=best_estimator.classes_)
conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_)
# Erstelle DataFrame für bessere Lesbarkeit der Matrix
conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_)
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_df}")
print(f"\nModell-Evaluation abgeschlossen. Genauigkeit (Test): {test_accuracy:.4f}")
print(f"Log für Details: {LOG_FILE}")
# 6. Muster extrahieren (Text)
debug_print("\nExtrahiere Regeln aus dem besten Baum...")
debug_print(f"\n--- Evaluation Test-Set ---\nGenauigkeit: {test_accuracy:.4f}\nBericht:\n{report}\nMatrix:\n{conf_matrix_df}"); print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}")
debug_print("\nExtrahiere Regeln...");
try:
feature_names = list(X_train.columns)
class_names = best_estimator.classes_
rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3) # Bessere Formatierung
# debug_print(f"--- Baumregeln (Text) ---:\n{rules_text}") # Kann sehr lang sein
patterns_filename = args.patterns_out # Verwende Argument oder Default
feature_names = list(X_train.columns); class_names = best_estimator.classes_
rules_text = export_text(best_estimator, feature_names=feature_names, class_names=class_names, show_weights=True, spacing=3)
patterns_filename = args.patterns_out;
with open(patterns_filename, 'w', encoding='utf-8') as f: f.write(rules_text)
debug_print(f"Regeln als Text gespeichert in '{patterns_filename}'.")
debug_print(f"Regeln gespeichert: '{patterns_filename}'.")
except Exception as e_export: debug_print(f"Fehler Export Regeln: {e_export}")
else: debug_print("Datenvorbereitung fehlgeschlagen -> Abbruch ML Training.")
else:
debug_print(f"Unbekannter Modus '{mode}'.")
except Exception as e: # Fange unerwartete Fehler im Hauptblock ab
except Exception as e:
debug_print(f"FATAL: Unerwarteter Fehler in main try-Block: {e}")
import traceback; debug_print(traceback.format_exc())