"""第 11 章：不提供标签，探索聚类、二维投影与离群分数。"""

import numpy as np
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler


def make_data():
    rng = np.random.default_rng(2026)
    xy = np.vstack([rng.normal(center, 0.65, (100, 2))
                    for center in [(-3, -1), (0, 3), (3, -1)]])
    third = 0.8*xy[:, 0] + 0.3*xy[:, 1] + rng.normal(0, 0.2, len(xy))
    return np.column_stack([xy, third])


def fit():
    x = make_data()
    scaler = StandardScaler().fit(x)
    z = scaler.transform(x)
    clustering = KMeans(n_clusters=3, n_init=10, random_state=2026).fit(z)
    pca = PCA(n_components=2, svd_solver="full").fit(z)
    return x, scaler, z, clustering, pca


def main():
    x, scaler, z, clustering, pca = fit()
    counts = np.bincount(clustering.labels_, minlength=3)
    print("三簇样本数（排序后）：" + str(sorted(counts.tolist())))
    ratios = pca.explained_variance_ratio_
    print("前两主成分解释方差比例：" + ", ".join(f"{r:.3f}" for r in ratios))
    print(f"合计保留方差比例：{ratios.sum():.3f}")
    reconstructed = pca.inverse_transform(pca.transform(z))
    print(f"标准化空间重建 MSE：{np.mean((z-reconstructed)**2):.4f}")
    distances = clustering.transform(z).min(axis=1)
    threshold = np.quantile(distances, 0.95)
    queries = np.array([[3, -1, 2.1], [0, 6, 1.8], [0, 3, 8.0]])
    scores = clustering.transform(scaler.transform(queries)).min(axis=1)
    print(f"训练距离的 95% 分位点：{threshold:.3f}")
    for i, score in enumerate(scores, start=1):
        print(f"新点 {i}：距离={score:.3f}，标记待检查={bool(score > threshold)}")


if __name__ == "__main__":
    main()
