一、问题引入

海伦一直使用在线约会网站寻找适合自己的约会对象。她曾交往过三种类型的人:

1.不喜欢的人

2.一般喜欢的人

3.非常喜欢的人

这些人包含以下三种特征:

1. 每年获得的飞行常客里程数

2. 玩视频游戏所耗时间百分比

3. 每周消费的冰淇淋公升数

该网站现在需要尽可能向海伦推荐她喜欢的人,需要我们设计一个分类器,根据用户的以上三种特征,识别出是否该向海伦推荐。

二、需求概要分析

根据问题,我们可知,样本特征个数为3,样本标签为三类。现需要实现将一个待分类样本的三个特征值输入程序后,能够识别该样本的类别,并且将该类别输出。

 三、K近邻算法的一般流程

1. 数据准备:这包括收集、清洗和预处理数据。预处理可能包括归一化或标准化特征,以确保所有特征在计算距离时具有相等的权重。

|      | 玩视频游戏所耗时间百分比 | 每年获得的飞行常客里程数 | 每周消费的冰淇淋的公升数 | 样本分类 |

| ---- | ------------------------ | ------------------------ | ------------------------ | -------- |

| 1    | 0.8                      | 400                      | 0.5                      | 1        |

| 2    | 12                       | 134000                   | 0.9                      | 3        |

| 3    | 0                        | 20000                    | 1.1                      | 2        |

| 4    | 67                       | 32000                    | 0.1                      | 2        |

我们很容易发现,当计算样本之间的距离时数字差值最大的属性对计算结果的影响最大,也就是说,每年获取的飞行常客里程数对于计算结果的影响将远远大于上表中其他两个特征-玩视频游戏所耗时间占比和每周消费冰淇淋公斤数的影响。而产生这种现象的唯一原因,仅仅是因为飞行常客里程数远大于其他特征值。但海伦认为这三种特征是同等重要的,因此作为三个等权重的特征之一,飞行常客里程数并不应该如此严重地影响到计算结果。

在处理这种不同取值范围的特征值时,我们通常采用的方法是将数值归一化,如将取值范围处理为0到1或者-1到1之间。

下面的公式可以将任意取值范围的特征值转化为0到1区间内的值:

2. 选择距离度量方法:确定用于比较样本之间相似性的度量方法,常见的如欧几里得距离、曼哈顿距离等。

3. 确定K值:选择一个K值,即在分类或回归时应考虑的邻居数量。这是一个超参数,可以通过交叉验证等方法来选择最优的K值。

4. 找到K个最近邻居:对于每一个需要预测的未标记的样本:

①计算该样本与训练集中所有样本的距离。

②根据距离对它们进行排序。

③选择距离最近的K个样本

5. 预测:

 对于分类任务:查看K个最近邻居中最常见的类别,作为预测结果。例如,如果K=3,并且三个最近邻居的类别是[1, 2, 1],那么预测结果就是类别1。

 对于回归任务:预测结果可以是K个最近邻居的平均值或加权平均值。

6. 评估:使用适当的评价指标(如准确率、均方误差等)评估模型的性能。

7. 优化:基于性能评估结果,可能需要返回并调整某些参数,如K值、距离度量方法等,以获得更好的性能。

三、模型评估

1.ROC曲线 (Receiver Operating Characteristic Curve)**

描述:在各种阈值设置下,真正例率 (TPR) 对假正例率 (FPR) 的曲线。**通过描绘两个指标之间的关系来展示分类器性能**

2.PR曲线 (Precision-Recall Curve)**

描述:在各种阈值设置下,精确率(Precision)和召回率(Recall)的曲线。**也是通过描绘两个指标之间的关系来展示分类器性能**

3.ROC曲线与PR曲线的差异

①坐标轴

ROC曲线的x轴是FPR,y轴是TPR。

PR曲线的x轴是Recall(或TPR),y轴是Precision。

②2. 应用场景

在正负样本均衡的情况下,ROC曲线是一个很好的性能度量。

在正负样本不均衡的情况下,PR曲线往往更有信息量。

③整体性能指标

ROC曲线使用AUC(曲线下的面积)。

PR曲线使用AP(Average Precision,曲线下的面积)。

五、算法实现

# 导入必备工具库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import roc_curve, precision_recall_curve, auc

# --------------------------
# 1. 加载并预处理数据
# --------------------------
# 定义列名,对应3个特征+1个标签
col_names = ["annual_flight_miles", "weekly_icecream", "weekly_game_hours", "label"]
# 读取TXT文件(分隔符为制表符,无表头)
data = pd.read_csv(
    "datingTestSet(2).txt",  # 确保文件路径正确,同级目录直接写文件名
    sep="\t",
    header=None,
    names=col_names
)

# 文本标签转数值(KNN算法需数值型标签)
label_encoder = LabelEncoder()
data["label_encoded"] = label_encoder.fit_transform(data["label"])
# 查看标签映射关系(方便后续解读)
print("标签映射关系:")
print(dict(zip(label_encoder.classes_, label_encoder.transform(label_encoder.classes_))))

# 构建二分类任务(ROC/PR曲线需二分类场景):无好感=0,有好感(中+高)=1
X = data[["annual_flight_miles", "weekly_icecream", "weekly_game_hours"]].values  # 特征矩阵
y = np.where(data["label_encoded"] == 0, 0, 1)  # 二分类标签

# --------------------------
# 2. 数据预处理:分割+标准化(KNN性能关键)
# --------------------------
# 分割训练集(70%)和测试集(30%),stratify=y保证标签分布一致
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 特征标准化:消除量纲差异(如里程数是万级,冰淇淋是个级,不标准化会主导分类)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 训练集:拟合+标准化
X_test_scaled = scaler.transform(X_test)        # 测试集:仅用训练集参数(避免数据泄露)

# --------------------------
# 3. KNN模型训练:调优核心参数n_neighbors
# --------------------------
# 测试K=1到20的效果,选择ROC-AUC最优的K值
best_auc = 0
best_k = 1
k_candidates = range(1, 21)

for k in k_candidates:
    # 初始化KNN模型,使用欧氏距离(默认)
    knn = KNeighborsClassifier(n_neighbors=k, metric="euclidean")
    knn.fit(X_train_scaled, y_train)
    # 获取正类(有好感=1)的预测概率(绘制曲线需概率值,而非硬分类结果)
    y_pred_proba = knn.predict_proba(X_test_scaled)[:, 1]
    # 计算当前K值的ROC-AUC
    fpr, tpr, _ = roc_curve(y_test, y_pred_proba)
    current_auc = auc(fpr, tpr)
    
    # 更新最优K值
    if current_auc > best_auc:
        best_auc = current_auc
        best_k = k

print(f"\n最优K值:{best_k},对应ROC-AUC:{best_auc:.3f}")

# 用最优K值训练最终模型
final_knn = KNeighborsClassifier(n_neighbors=best_k, metric="euclidean")
final_knn.fit(X_train_scaled, y_train)
# 生成测试集预测结果(概率+准确率)
y_pred_proba_final = final_knn.predict_proba(X_test_scaled)[:, 1]
test_acc = final_knn.score(X_test_scaled, y_test)

# --------------------------
# 4. 计算ROC与PR曲线关键指标
# --------------------------
# ROC曲线:假正例率(FPR)、真正例率(TPR)
fpr_final, tpr_final, _ = roc_curve(y_test, y_pred_proba_final)
roc_auc_final = auc(fpr_final, tpr_final)

# PR曲线:精确率(Precision)、召回率(Recall)
precision_final, recall_final, _ = precision_recall_curve(y_test, y_pred_proba_final)
pr_auc_final = auc(recall_final, precision_final)

# --------------------------
# 5. 绘制ROC与PR曲线(中文适配+清晰标注)
# --------------------------
plt.rcParams['font.sans-serif'] = ['SimHei']  # 解决中文乱码
plt.rcParams['axes.unicode_minus'] = False    # 解决负号显示问题

# 创建1行2列的子图,控制图的大小与布局
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

# 子图1:ROC曲线
ax1.plot(fpr_final, tpr_final, color="#FF5252", linewidth=2.5, 
         label=f'KNN ROC曲线 (K={best_k}, AUC={roc_auc_final:.3f})')
ax1.plot([0, 1], [0, 1], color="#40C4FF", linewidth=2, linestyle='--', 
         label='随机猜测(基准线)')
ax1.set_xlabel('假正例率 (FPR)', fontsize=12)
ax1.set_ylabel('真正例率 (TPR)', fontsize=12)
ax1.set_title('约会数据集KNN分类器ROC曲线', fontsize=14, fontweight='bold')
ax1.legend(loc='lower right', fontsize=10)
ax1.grid(alpha=0.3)  # 添加网格,便于读取具体数值
ax1.set_xlim([0.0, 1.0])
ax1.set_ylim([0.0, 1.05])

# 子图2:PR曲线
ax2.plot(recall_final, precision_final, color="#4CAF50", linewidth=2.5, 
         label=f'KNN PR曲线 (K={best_k}, AUC={pr_auc_final:.3f})')
ax2.set_xlabel('召回率 (Recall)', fontsize=12)
ax2.set_ylabel('精确率 (Precision)', fontsize=12)
ax2.set_title('约会数据集KNN分类器PR曲线', fontsize=14, fontweight='bold')
ax2.legend(loc='lower left', fontsize=10)
ax2.grid(alpha=0.3)
ax2.set_xlim([0.0, 1.0])
ax2.set_ylim([0.0, 1.05])

# 自动调整子图间距,避免标签重叠
plt.tight_layout()
# 显示图片
plt.show()

# --------------------------
# 6. 输出模型性能报告
# --------------------------
print("\n=== 约会数据集KNN分类模型性能报告 ===")
print(f"最优K值:{best_k}")
print(f"测试集准确率:{test_acc:.3f}")
print(f"ROC曲线下面积 (AUC):{roc_auc_final:.3f}")
print(f"PR曲线下面积 (AUC):{pr_auc_final:.3f}")

更多推荐