# -*- coding: utf-8 -*-
"""
Created on Mon Nov 10 10:20:19 2025

@author: Moritz Romeike
"""

# --------------------------------------------------------------------------------
# Programmcode 34 (Python): Multiple logistische Regression – Forderungsausfallrisiko
# Paketinstallation automatisiert (statsmodels, pandas, matplotlib, scipy)
# --------------------------------------------------------------------------------

import sys, subprocess

def ensure_package(mod_name, pip_name=None):
    try:
        return __import__(mod_name)
    except ModuleNotFoundError:
        print(f"[Setup] Paket '{mod_name}' nicht gefunden. Installiere über pip ...")
        subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name or mod_name])
        return __import__(mod_name)

# Pakete sicherstellen
np = ensure_package("numpy")
pd = ensure_package("pandas")
ensure_package("matplotlib")
ensure_package("statsmodels")
ensure_package("scipy")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
import statsmodels.formula.api as smf
from scipy.stats import chi2

# --------------------------------------------------------------------------------
# Daten einlesen 
# --------------------------------------------------------------------------------
csv_path = "data_logregression_multivariat.csv"

def read_csv_robust(path: str) -> pd.DataFrame:
    """Zuerst read.csv2-ähnlich versuchen; wenn nur 1 Spalte erkannt wird, Standard-CSV lesen."""
    try:
        df = pd.read_csv(path, sep=";", decimal=",", dtype=str, encoding="utf-8", engine="python")
        if df.shape[1] == 1:
            raise ValueError("Nur 1 Spalte erkannt -> Standard-CSV probieren.")
    except Exception:
        df = pd.read_csv(path, dtype=str, encoding="utf-8", engine="python")
    return df

df_raw = read_csv_robust(csv_path)
df_raw.columns = df_raw.columns.str.strip()

# Erwartete Spalten
required = ["dauer", "moral", "Ausfall"]
missing = [c for c in required if c not in df_raw.columns]
if missing:
    raise KeyError(f"Fehlende Spalten {missing}. Vorhanden: {list(df_raw.columns)}")

def to_numeric_series_auto(s: pd.Series) -> pd.Series:
    """
    Robuste Zahlkonvertierung:
    - Wenn irgendein Komma vorkommt -> ',' als Dezimaltrennzeichen, '.' als Tausenderpunkt entfernen.
    - Sonst -> '.' als Dezimalpunkt, ',' als Tausenderkomma entfernen.
    """
    s = s.astype(str).str.strip()
    if s.str.contains(",", regex=False).any():
        # de-Format: z.B. "1.234,56"
        s = s.str.replace(".", "", regex=False)   # Tausenderpunkt weg
        s = s.str.replace(",", ".", regex=False)  # Dezimalkomma -> Punkt
    else:
        # en-Format: z.B. "1,234.56" oder "1234.56"
        s = s.str.replace(",", "", regex=False)   # Tausenderkomma weg
        # Dezimalpunkt bleibt erhalten
    return pd.to_numeric(s, errors="coerce")

df = df_raw.copy()
df["dauer"] = to_numeric_series_auto(df["dauer"])
df["moral"] = to_numeric_series_auto(df["moral"])

# Ausfall 0/1 säubern
map_yesno = {"ja": "1", "nein": "0", "wahr": "1", "falsch": "0", "true": "1", "false": "0"}
df["Ausfall"] = df["Ausfall"].astype(str).str.strip().str.lower().replace(map_yesno)
df["Ausfall"] = to_numeric_series_auto(df["Ausfall"]).astype("Int64")

# NA-Zeilen raus
df = df.dropna(subset=["dauer", "moral", "Ausfall"]).copy()
df["Ausfall"] = df["Ausfall"].astype(int)

print(f"[Info] Daten geladen: {df.shape[0]} Beobachtungen")

# --------------------------------------------------------------------------------
# Streudiagramm (Dauer vs. Moral)
# --------------------------------------------------------------------------------
plt.figure(figsize=(7, 5))
mask1 = df["Ausfall"] == 1
mask0 = df["Ausfall"] == 0
plt.scatter(df.loc[mask1, "dauer"], df.loc[mask1, "moral"], c="#00BFFF", marker="o", label="Ausfall", s=35)
plt.scatter(df.loc[mask0, "dauer"], df.loc[mask0, "moral"], c="#FFA500", marker="x", label="Nicht-Ausfall", s=35)
plt.xlabel("Dauer der Geschäftsbeziehung [in Monaten]")
plt.ylabel("Zahlungsmoral [in Anzahl der Ausfälle]")
plt.legend(loc="upper right", frameon=False)
plt.grid(True, alpha=0.25)
plt.tight_layout()
plt.show()

# --------------------------------------------------------------------------------
# Modelle
# --------------------------------------------------------------------------------
glm_null  = smf.glm("Ausfall ~ 1",             data=df, family=sm.families.Binomial()).fit()
glm_dauer = smf.glm("Ausfall ~ dauer",         data=df, family=sm.families.Binomial()).fit()
glm_moral = smf.glm("Ausfall ~ moral",         data=df, family=sm.families.Binomial()).fit()
glm_full  = smf.glm("Ausfall ~ dauer + moral", data=df, family=sm.families.Binomial()).fit()

print("\n=== Multiples logit-Modell ===")
print(glm_full.summary())

# R-ähnliche Kennzahlen
n_obs  = glm_full.nobs
df_null = n_obs - 1
print(f"\nNull deviance:     {getattr(glm_full,'null_deviance', np.nan):.2f} on {df_null:.0f} DF")
print(f"Residual deviance: {glm_full.deviance:.2f} on {glm_full.df_resid:.0f} DF")
print(f"AIC:               {glm_full.aic:.2f}")

# --------------------------------------------------------------------------------
# Analysis of Deviance (sequenziell: NULL -> +dauer -> +moral)
# --------------------------------------------------------------------------------
def deviance_table_sequential(df_in: pd.DataFrame) -> pd.DataFrame:
    rows = []
    m0 = smf.glm("Ausfall ~ 1", data=df_in, family=sm.families.Binomial()).fit()
    rows.append(dict(Step="NULL", Df=np.nan, Deviance=np.nan,
                     Resid_Df=m0.df_resid, Resid_Dev=m0.deviance, Pr_Chi=np.nan))
    m1 = smf.glm("Ausfall ~ dauer", data=df_in, family=sm.families.Binomial()).fit()
    dfdiff = int(m0.df_resid - m1.df_resid)
    devdiff = m0.deviance - m1.deviance
    pval = chi2.sf(devdiff, dfdiff)
    rows.append(dict(Step="dauer", Df=dfdiff, Deviance=devdiff,
                     Resid_Df=m1.df_resid, Resid_Dev=m1.deviance, Pr_Chi=pval))
    m2 = smf.glm("Ausfall ~ dauer + moral", data=df_in, family=sm.families.Binomial()).fit()
    dfdiff = int(m1.df_resid - m2.df_resid)
    devdiff = m1.deviance - m2.deviance
    pval = chi2.sf(devdiff, dfdiff)
    rows.append(dict(Step="moral", Df=dfdiff, Deviance=devdiff,
                     Resid_Df=m2.df_resid, Resid_Dev=m2.deviance, Pr_Chi=pval))
    return pd.DataFrame(rows)

anova_df = deviance_table_sequential(df)
print("\n=== Analysis of Deviance (Chi^2) ===")
print(anova_df.to_string(index=False,
                         formatters={"Deviance": "{:.3f}".format,
                                     "Resid_Dev": "{:.3f}".format,
                                     "Pr_Chi": lambda v: f"{v:.3e}" if pd.notna(v) else ""}))

# --------------------------------------------------------------------------------
# Boxplots: Dauer ~ Ausfall und Moral ~ Ausfall
# --------------------------------------------------------------------------------
fig, axes = plt.subplots(1, 2, figsize=(10, 4.5))

# === Erstes Boxplot: Dauer ~ Ausfall ===
data_dauer = [
    df.loc[df["Ausfall"] == 0, "dauer"].dropna().to_numpy(),
    df.loc[df["Ausfall"] == 1, "dauer"].dropna().to_numpy()
]
b1 = axes[0].boxplot(
    data_dauer,
    labels=["0", "1"],
    patch_artist=True,
    widths=0.6
)
# Farben + Rahmen
for patch, color in zip(b1["boxes"], ["#00BFFF", "#FF8C00"]):  # deepskyblue, darkorange
    patch.set_facecolor(color)
    patch.set_edgecolor("#000000")

axes[0].set_xlabel("Ausfall", fontsize=10)
axes[0].set_ylabel("Dauer der Geschäftsbeziehung [Monate]", fontsize=10)
axes[0].set_title("Dauer ~ Ausfall", fontsize=11)
axes[0].grid(True, axis="y", alpha=0.3)

# === Zweites Boxplot: Moral ~ Ausfall ===
data_moral = [
    df.loc[df["Ausfall"] == 0, "moral"].dropna().to_numpy(),
    df.loc[df["Ausfall"] == 1, "moral"].dropna().to_numpy()
]
b2 = axes[1].boxplot(
    data_moral,
    labels=["0", "1"],
    patch_artist=True,
    widths=0.6
)
for patch, color in zip(b2["boxes"], ["#00BFFF", "#FF8C00"]):
    patch.set_facecolor(color)
    patch.set_edgecolor("#000000")

axes[1].set_xlabel("Ausfall", fontsize=10)
axes[1].set_ylabel("Zahlungsmoral", fontsize=10)
axes[1].set_title("Moral ~ Ausfall", fontsize=11)
axes[1].grid(True, axis="y", alpha=0.3)

# === Layout-Optimierung ===
plt.subplots_adjust(left=0.08, right=0.97, bottom=0.12, top=0.88, wspace=0.28)
plt.show()

# --------------------------------------------------------------------------------
# Devianzen-Balkendiagramm
# --------------------------------------------------------------------------------
deviances = [glm_null.deviance, glm_dauer.deviance, glm_moral.deviance, glm_full.deviance]
labels    = ["Null-Modell", "Dauer", "Zahlungsmoral", "Multiples Modell"]
bar_colors = ["#E5E5E5", "#00BFFF", "#FF8C00", "#999999"]

plt.figure(figsize=(7.5, 4.5))
plt.bar(range(len(deviances)), deviances, color=bar_colors, edgecolor="#000000")
plt.xticks(range(len(labels)), labels)
plt.xlabel("Modelle")
plt.ylabel("Devianz")
plt.title("Devianzen der logistischen Regressionsmodelle")
plt.ylim(0, max(deviances) * 1.2)
for i, v in enumerate(deviances):
    plt.text(i, v, f"{v:.2f}", ha="center", va="bottom", fontsize=9)
plt.tight_layout()
plt.show()

# --------------------------------------------------------------------------------
# Vorwärtsselektion (AIC) + AIC-Kurvenplot
# --------------------------------------------------------------------------------
def forward_step_aic(df_in: pd.DataFrame, response: str, candidates: list[str], start=None):
    included = [] if start is None else start[:]

    def fit_model(preds):
        formula = f"{response} ~ " + " + ".join(preds) if preds else f"{response} ~ 1"
        res = smf.glm(formula, data=df_in, family=sm.families.Binomial()).fit()
        return res, formula

    model, _ = fit_model(included)   # Nullmodell
    aic_path = [model.aic]
    improved = True
    while improved:
        improved = False
        best = (np.inf, None, None)
        for c in candidates:
            if c in included:
                continue
            trial = included + [c]
            res, _ = fit_model(trial)
            if res.aic < best[0]:
                best = (res.aic, c, res)
        if best[1] is not None and best[0] + 1e-9 < model.aic:
            included.append(best[1])
            model = best[2]
            aic_path.append(model.aic)
            improved = True
    return model, included, aic_path

final_model, selected, aic_path = forward_step_aic(df, "Ausfall", ["moral", "dauer"])

print("\n=== Stepwise Forward Selection (AIC) ===")
print("Gewählte Prädiktoren:", selected)
print("Finales Modell:", final_model.model.formula)

# AIC-Verlauf plotten
k_vals = list(range(len(aic_path)))  # 0 = Nullmodell
plt.figure(figsize=(7.5, 4.5))
plt.plot(k_vals, aic_path, marker="o", linestyle="-")
plt.xlabel("Anzahl der Einflussgrößen")
plt.ylabel("AIC-Wert")
plt.title("AIC-Werte während der Vorwärts-Selektion")
plt.xticks(k_vals, k_vals)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# --------------------------------------------------------------------------------
