"""
Reproduce: "Who's about to ask for a mortgage?"
------------------------------------------------
End-to-end script for the mortgage-propensity investigation on
Data and Other Dangerous Things.

Rebuilds the synthetic dataset (100k customers), engineers 15 features,
trains Logistic Regression, Decision Tree and Random Forest, and prints
ROC-AUC / Precision / Recall plus the top feature importances.

Run:
    pip install numpy pandas scikit-learn
    python mortgage-propensity.py

Read-only: do not modify this file in place — copy it first.
"""
from __future__ import annotations

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, precision_score, recall_score

RNG = np.random.default_rng(42)
N = 100_000

# ---------------------------------------------------------------- 1. Data
regions = ["north", "south", "east", "west", "central"]
df = pd.DataFrame({
    "salary":             RNG.lognormal(10.6, 0.45, N).round(0),
    "tenure_months":      RNG.integers(1, 240, N),
    "product_count":      RNG.integers(1, 8, N),
    "app_logins_30d":     RNG.poisson(12, N),
    "mortgage_visits_30d":RNG.poisson(0.4, N),
    "savings_balance":    RNG.lognormal(8.8, 1.1, N).round(0),
    "email_engagement":   RNG.beta(2, 5, N),
    "prev_campaign_resp": RNG.integers(0, 2, N),
    "salary_credited":    RNG.integers(0, 2, N),
    "region":             RNG.choice(regions, N),
})

# Ground-truth propensity — behaviour dominates, demographics assist.
logit = (
    -6.0
    + 0.9 * (df["mortgage_visits_30d"] > 0).astype(int)
    + 0.6 * df["email_engagement"]
    + 0.4 * df["prev_campaign_resp"]
    + 0.0000025 * df["salary"]
    + 0.002 * df["tenure_months"]
    + 0.15 * (df["product_count"] >= 3).astype(int)
)
p = 1 / (1 + np.exp(-logit))
df["applied_mortgage"] = (RNG.uniform(0, 1, N) < p).astype(int)

# ---------------------------------------------------------------- 2. Features
df["log_salary"]        = np.log1p(df["salary"])
df["log_savings"]       = np.log1p(df["savings_balance"])
df["tenure_years"]      = df["tenure_months"] / 12
df["visits_per_login"]  = df["mortgage_visits_30d"] / (df["app_logins_30d"] + 1)
df["engaged_visitor"]   = ((df["mortgage_visits_30d"] > 0) &
                           (df["email_engagement"] > 0.3)).astype(int)
df["savings_to_salary"] = df["savings_balance"] / (df["salary"] + 1)
df["is_multi_product"]  = (df["product_count"] >= 3).astype(int)
df["is_new_customer"]   = (df["tenure_months"] < 12).astype(int)
df["salary_bucket"]     = pd.qcut(df["salary"], 5, labels=False)

numeric = [
    "log_salary", "log_savings", "tenure_years", "product_count",
    "app_logins_30d", "mortgage_visits_30d", "email_engagement",
    "visits_per_login", "savings_to_salary", "salary_bucket",
]
binary  = ["prev_campaign_resp", "salary_credited",
           "engaged_visitor", "is_multi_product", "is_new_customer"]
categorical = ["region"]

X = df[numeric + binary + categorical]
y = df["applied_mortgage"]

# ---------------------------------------------------------------- 3. Split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.20, stratify=y, random_state=42,
)

# ---------------------------------------------------------------- 4. Preproc
# - StandardScaler on numeric (needed for Logistic Regression)
# - OneHotEncoder on region
# - Binary flags pass through untouched
preproc = ColumnTransformer([
    ("num", StandardScaler(), numeric),
    ("cat", OneHotEncoder(handle_unknown="ignore"), categorical),
    ("bin", "passthrough", binary),
])

# ---------------------------------------------------------------- 5. Models
models = {
    "LogisticRegression": LogisticRegression(max_iter=1000, class_weight="balanced"),
    "DecisionTree":       DecisionTreeClassifier(max_depth=8, class_weight="balanced",
                                                  random_state=42),
    "RandomForest":       RandomForestClassifier(n_estimators=400, max_depth=12,
                                                  class_weight="balanced_subsample",
                                                  n_jobs=-1, random_state=42),
}

print(f"{'model':<20} {'roc_auc':>8} {'precision':>10} {'recall':>8}")
print("-" * 48)
fitted = {}
for name, clf in models.items():
    pipe = Pipeline([("prep", preproc), ("clf", clf)])
    pipe.fit(X_train, y_train)
    proba = pipe.predict_proba(X_test)[:, 1]
    preds = (proba >= 0.5).astype(int)
    print(f"{name:<20} "
          f"{roc_auc_score(y_test, proba):>8.3f} "
          f"{precision_score(y_test, preds, zero_division=0):>10.3f} "
          f"{recall_score(y_test, preds):>8.3f}")
    fitted[name] = pipe

# ---------------------------------------------------------------- 6. Importance
rf = fitted["RandomForest"]
feature_names = rf.named_steps["prep"].get_feature_names_out()
importances = rf.named_steps["clf"].feature_importances_
top = (pd.Series(importances, index=feature_names)
         .sort_values(ascending=False).head(10))
print("\ntop features (random forest):")
print(top.round(3).to_string())
