# -*- coding: utf-8 -*-
"""
Created on Wed Sep  3 15:31:47 2025

@author: Administrator
"""


def plot_lm_diagnostics(model, n_labels=3):
    """
    模仿 R 的 plot.lm()，绘制四张回归诊断图并标注异常点
    n_labels: 每张图标注残差绝对值最大的点数
    """
    import numpy as np
    import matplotlib.pyplot as plt
    import statsmodels.api as sm
    from statsmodels.nonparametric.smoothers_lowess import lowess
    fitted = model.fittedvalues
    resid = model.resid
    influence = model.get_influence()
    student_resid = influence.resid_studentized_internal
    leverage = influence.hat_matrix_diag
    norm_resid = sm.ProbPlot(resid)

    fig, axes = plt.subplots(2, 2, figsize=(12, 10))
    ax = axes.flatten()

    # ---- 1. Residuals vs Fitted ----
    ax[0].scatter(fitted, resid, edgecolors="k", alpha=0.7)
    ax[0].axhline(0, color="red", linestyle="--")
    smooth = lowess(resid, fitted)
    ax[0].plot(smooth[:, 0], smooth[:, 1], color="blue", lw=2)
    ax[0].set_xlabel("Fitted values")
    ax[0].set_ylabel("Residuals")
    ax[0].set_title("Residuals vs Fitted")
    # 标注异常点
    largest = np.argsort(np.abs(resid))[-n_labels:]
    for i in largest:
        ax[0].annotate(i, (fitted[i], resid[i]))

    # ---- 2. Normal Q-Q ----
    sm.qqplot(resid, line="45", fit=True, ax=ax[1])
    ax[1].set_title("Normal Q-Q")
    # 标注异常点
    largest = np.argsort(np.abs(resid))[-n_labels:]
    sorted_resid = np.sort(resid)
    for i in largest:
        ax[1].annotate(i, (norm_resid.theoretical_quantiles[i], sorted_resid[i]))

    # ---- 3. Scale-Location ----
    sqrt_std_resid = np.sqrt(np.abs(student_resid))
    ax[2].scatter(fitted, sqrt_std_resid, edgecolors="k", alpha=0.7)
    smooth = lowess(sqrt_std_resid, fitted)
    ax[2].plot(smooth[:, 0], smooth[:, 1], color="blue", lw=2)
    ax[2].set_xlabel("Fitted values")
    ax[2].set_ylabel(r"$\sqrt{|Standardized Residuals|}$")
    ax[2].set_title("Scale-Location")
    # 标注异常点
    largest = np.argsort(sqrt_std_resid)[-n_labels:]
    for i in largest:
        ax[2].annotate(i, (fitted[i], sqrt_std_resid[i]))

    # ---- 4. Residuals vs Leverage ----
    ax[3].scatter(leverage, student_resid, edgecolors="k", alpha=0.7)
    ax[3].axhline(0, color="red", linestyle="--")
    smooth = lowess(student_resid, leverage)
    ax[3].plot(smooth[:, 0], smooth[:, 1], color="blue", lw=2)
    ax[3].set_xlabel("Leverage")
    ax[3].set_ylabel("Studentized Residuals")
    ax[3].set_title("Residuals vs Leverage")
    # 标注异常点
    largest = np.argsort(np.abs(student_resid))[-n_labels:]
    for i in largest:
        ax[3].annotate(i, (leverage[i], student_resid[i]))

    plt.tight_layout()
    plt.show()


def stepwise_selection_aic(X, y):
    """  完整逐步回归 (Stepwise) 基于 AIC
    返回每一步操作和最终选出的特征
    """
    import statsmodels.api as sm
    from itertools import combinations
    import pandas as pd
    best_features = []
    remaining_features = list(X.columns)
    current_score = float('inf')
    step = 1
    history = []

    # --- 前向选择 ---
    while remaining_features:
        scores_with_candidates = []
        for candidate in remaining_features:
            features_to_test = best_features + [candidate]
            X_train = sm.add_constant(X[features_to_test])
            model = sm.OLS(y, X_train).fit()
            scores_with_candidates.append((model.aic, candidate))
        scores_with_candidates.sort()
        best_new_score, best_candidate = scores_with_candidates[0]

        if best_new_score < current_score:
            best_features.append(best_candidate)
            remaining_features.remove(best_candidate)
            current_score = best_new_score
            history.append({
                "step": step,
                "action": "Add",
                "feature": best_candidate,
                "AIC": current_score
            })
            step += 1
        else:
            break

    # --- 后向剔除 ---
    while len(best_features) > 1:
        scores_with_candidates = []
        for combo in combinations(best_features, len(best_features)-1):
            X_train = sm.add_constant(X[list(combo)])
            model = sm.OLS(y, X_train).fit()
            scores_with_candidates.append((model.aic, combo))
        scores_with_candidates.sort()
        best_new_score, best_combo = scores_with_candidates[0]

        if best_new_score < current_score:
            removed_feature = list(set(best_features) - set(best_combo))[0]
            best_features = list(best_combo)
            current_score = best_new_score
            history.append({
                "step": step,
                "action": "Remove",
                "feature": removed_feature,
                "AIC": current_score
            })
            step += 1
        else:
            break

    return best_features, pd.DataFrame(history)


def align_labels(y_true, y_pred):
    """
    对齐聚类标签，使其与真实标签对应。
    参数：
        y_true : array-like, shape (n_samples,)
            真实标签
        y_pred : array-like, shape (n_samples,)
            聚类预测标签（0~n_clusters-1）
    返回：
        y_pred_aligned : np.array, shape (n_samples,)
            对齐后的聚类标签
        accuracy : float
            对齐后与真实标签的准确率
    """
    import numpy as np

    y_true = np.array(y_true)
    y_pred = np.array(y_pred)
    labels_map = {}
    for cluster in np.unique(y_pred):
        mask = (y_pred == cluster)
        # 簇内出现次数最多的真实标签
        labels_map[cluster] = mode(y_true[mask], keepdims=True).mode[0]
    # 对齐后的预测标签
    y_pred_aligned = np.array([labels_map[c] for c in y_pred])
    # 计算对齐后的准确率   #accuracy = accuracy_score(y_true, y_pred_aligned)
    return y_pred_aligned



"""
判断是否加载了包，没有加载则安装，否则不用安装

"""


def install_if_needed(package):
    try:
        __import__(package)
        print(f"{package} 已安装")
    except ImportError:
        import os
        print(f"正在安装 {package}...")
        os.system(f"pip install {package} -q")
        print(f"{package} 安装完成")


def read_csv_with_headers(url: str):
    """
    参数:
        url (str): CSV 文件的网址
    返回:
        pd.DataFrame: 数据表
    """
    import pandas as pd
    import requests
    from io import StringIO
    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/117.0 Safari/537.36"
        )
    }
    response = requests.get(url, headers=headers)
    response.raise_for_status()   # 如果请求失败会抛出异常
    return pd.read_csv(StringIO(response.text), keep_default_na=False)











