KMeans聚类算法在高校宿舍分配中的应用:特征设计与组队策略全解析
发布时间:2026/10/2 8:31:54来源:尧图网络
简介这是一份面向高校信息化管理、后勤宿管及数据分析初学者的KMeans聚类实战资源围绕“学生画像—自动分寝—结果评估”完整流程展开核心提供了宿舍分配Python源码、可直接读取的聚类结果CSV与聚类中心占比CSV、程序运行演示MP4及文档演示MP4便于对照理解算法调参与可视化输出。压缩包共13个文件主要涵盖py脚本、CSV数据、mp4演示、txt说明、md文档及项目配置iml/xml整体仅10.71MB结构简洁、上手门槛低。资源已有820人学习下载适合想通过具体校园场景快速掌握KMeans用法、并迁移到其他聚类任务的读者使用阅读后可清楚了解数据预处理、K值选择与分寝结果评价的落地思路。1. KMeans聚类算法在高校宿舍分配里到底解决什么问题每年开学季宿舍分配都是让辅导员和宿管头疼的事新生上千人按班排还是按学号排作息冲突、生活习惯不合、晚睡早起互相干扰住进去一周就有人申请调宿。很多人第一反应是搞一套“性格测试匹配算法”但落地时发现数据收不上来、规则定义不清、结果无法解释。换个思路用KMeans聚类算法对学生的行为特征做无监督分组让相似的室友自动聚到同一类再从每一类里组队分配床位反而能跑通。python源码级别的实现并不复杂sklearn几十行就能解决难点在特征设计和分配策略。这篇文章就是讲清楚这件事的边界:什么样的数据适合做KMeans宿舍分配、特征怎么构造、k值怎么选、结果怎么落地成宿舍名单以及我实际跑数据时踩过的那些坑。适合手里有学生问卷数据、想要一套可解释的分配方案、又不想上复杂推荐系统的从业者。带演示视频和源码的那类工程包思路也跳不出这套流程。2. KMeans宿舍分配建模为什么这类问题适合聚类特征怎么构造2.1 KMeans在宿舍分配里的角色不是直接分房而是分人群常见的一个误解是KMeans输出每个学生的簇标签然后直接把同一个簇的人塞进同一间宿舍。这在小规模比如几十人时可行但上千人时会出现一个问题——同一个簇的人数不等于宿舍容量整数倍最后总会留下零零散散的“剩人”。所以更常见的做法是分层KMeans先做人群分层把相似的人聚成若干大群再在簇内做宿舍组队用贪心或随机抽样的方式填满每间宿舍。这个角色定位很重要。KMeans做的事情是把“宿舍分配”这个大问题拆成“找同类”和“组队入住”两个阶段。前者是聚类后者是组合分配两件事分开处理边界清楚得多。KMeans之所以适合这个场景是因为宿舍矛盾大多来自“特征距离远”——作息差异大、生活习惯不同。欧几里得距离能很好地度量这种差异。特征维度不多一般5到15个数据量也就是几千条KMeans时间复杂度接近线性跑一次秒级完成。换成DBSCAN这类密度聚类反而会因为数据分布均匀而效果平平。2.2 学生特征怎么设计字段、类型、编码方式特征设计是整套方案里决定上限的环节。我在实际项目中用过这个特征模板字段控制在10个以内学生填问卷不费劲聚类结果也稳定。特征字段取值示例编码方式作息偏好早睡型/正常/晚睡型数值化:22、23、24(表示期望睡觉时间)睡眠敏感度对光线/声音敏感程度 1-5等距数值是否吸烟是/否0/1卫生习惯1-5分等距数值是否打游戏从不/偶尔/经常0/1/2访客频率1-5分等距数值年级大一/大二/大三/大四1/2/3/4专业文本不参与距离计算,只做结果校验学习时段偏好宿舍学习/图书馆学习0/1注意“专业”这一列不能直接喂给KMeans。如果编码成1、2、3……数字欧几里得距离会强行认为“专业1”和“专业3”的距离是“专业1”和“专业2”的两倍毫无意义。专业只用于后续验证——“同一宿舍的人专业分布是否合理”。睡眠敏感度和卫生习惯这类李克特量表数据1到5分可以直接作为等距数值处理。这是从业者常用的做法严格来说这种量表是定序数据但在聚类场景下五个档位间的差值已经足够近似等距实际聚类效果不会因为这个理论瑕疵而明显变差。2.3 距离计算前必须做的两件事归一化和特征权重特征里混着22、23期望睡觉时间和3、4、5分数量表如果不处理KMeans的距离会被数值大的特征主导。比如“作息偏好”的取值范围是22到24方差大“是否吸烟”只有0和1方差小。算欧几里得距离时作息差1小时就相当于吸烟差3到4个级别这显然不合理。我一般的做法是先做Z-score标准化StandardScaler把每个特征变成均值为0、方差为1的分布。但标准化不等于权重相同。比如“是否吸烟”这个特征对宿舍矛盾的预测力很强但它只有0/1两个值标准化后方差也不大。我一般会单独给它加一个权重系数比如2.0相当于在距离计算里把吸烟差异放大两倍。如果项目要求特征权重可配置不要在sklearn的KMeans里调——它没有特征权重参数。正确做法是在标准化之后、聚类之前手动把特征向量乘以权重向量。这是源码设计里最容易被忽略的细节。3. 用Python把KMeans宿舍分配跑起来完整流程、代码拆解与参数说明3.1 数据集构造与预处理给演示脚本准备一份模拟数据学习这份源码时你手头大概率没有真实的几千份问卷数据。演示用数据可以直接用numpy生成。一份可复用的模拟数据脚本长这样import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler # 固定随机种子保证演示可复现避免每次生成的数据都不一样 np.random.seed(42) n_students 900 # 每个特征对应 2.2 节模板中的一列 # 作息偏好22点睡、23点睡、24点睡的人群大约 30%/45%/25% sleep_time np.random.choice([22, 23, 24], sizen_students, p[0.3, 0.45, 0.25]) # 睡眠敏感度1到5分偏向敏感的人略多 sensitivity np.random.randint(1, 6, sizen_students) # 是否吸烟约两成学生吸烟 smoke np.random.choice([0, 1], sizen_students, p[0.8, 0.2]) # 卫生习惯2到5分大部分集中在3和4 cleanliness np.random.randint(2, 6, sizen_students) # 打游戏频率0从不、1偶尔、2经常 game_freq np.random.choice([0, 1, 2], sizen_students, p[0.25, 0.5, 0.25]) # 访客频率1到5分 visitor_freq np.random.randint(1, 6, sizen_students) # 年级大一到大四 grade np.random.choice([1, 2, 3, 4], sizen_students, p[0.4, 0.3, 0.2, 0.1]) # 是否在宿舍学习0否、1是 study_in_dorm np.random.choice([0, 1], sizen_students, p[0.5, 0.5]) data pd.DataFrame({ sleep_time: sleep_time, sensitivity: sensitivity, smoke: smoke, cleanliness: cleanliness, game_freq: game_freq, visitor_freq: visitor_freq, grade: grade, study_in_dorm: study_in_dorm }) # 后四列可以拼一个“宿舍学习指数”按业务理解决定是否保留 data[study_score] data[study_in_dorm] * 2 data[cleanliness] * 0.5 print(data.head())参数说明np.random.seed(42)必须固定。KMeans的结果受初始质心影响而初始质心是随机生成的。seed固定后每次运行生成的数据和聚类结果都一样这对排查问题和写演示视频脚本极重要。p[0.3, 0.45, 0.25]是每个取值的概率用来模拟真实分布。比如24点睡的学生占比25%实际问卷统计出来的比例可能不一样改这个参数就能模拟不同学校的作息分布。study_score是特征工程的示例把两个强相关特征做加权合并相当于手动降维减少KMeans对冗余特征的敏感度。标准化这一步我习惯放到聚类前单独做不写进KMeans里# 参与聚类的是全部9列特征 feature_cols [sleep_time, sensitivity, smoke, cleanliness, game_freq, visitor_freq, grade, study_in_dorm, study_score] # 先复制一份数据保留原始字段用于结束后核对结果 X data[feature_cols].copy() # StandardScaler把每个特征变成均值0、方差1 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 给“是否吸烟”额外加权吸烟差异对宿舍矛盾影响大 weights np.array([1.0, 1.0, 2.5, 1.0, 1.2, 1.0, 0.8, 1.0, 0.8]) X_scaled X_scaled * weights print(标准化并加权后的数据形状:, X_scaled.shape)逻辑说明StandardScaler().fit_transform(X)先算每一列的均值和标准差再把每个值减去均值除以标准差。注意必须在全量数据上fit不能在每个簇内单独标准化否则距离失去可比性。权重数组里smoke对应位置是2.5意味着“一个吸烟学生和一个不吸烟学生”之间的距离被拉大到相当于“卫生习惯差2.5个标准差”。这个权重需要后续通过轮廓系数和宿舍投诉率反复调不是越极端越好。grade的权重设成0.8表示年级差异对舍友匹配的影响不大跨年级混住有时反而更和谐。这也是宿舍分配和一般聚类任务的不同不是把所有差异都放大有些差异需要刻意弱化。3.2 确定k值肘部法则和轮廓系数结合而不是拍脑袋k值簇数是KMeans里最敏感的超参数。它直接决定“人群被分成几类”——k太小宿舍矛盾多的学生混在一起k太大每个簇只剩几十人组宿舍时反而少了随机调度空间。我一般用两步走。第一步看肘部曲线第二步算轮廓系数两个都满意才定k。from sklearn.cluster import KMeans import matplotlib.pyplot as plt # 计算k从2到10的SSE簇内误差平方和和轮廓系数 sse [] silhouette_scores [] from sklearn.metrics import silhouette_score k_range range(2, 11) for k in k_range: km KMeans(n_clustersk, initk-means, n_init10, random_state42, max_iter300) labels km.fit_predict(X_scaled) sse.append(km.inertia_) # 样本量900抽样用samples300加速轮廓系数计算 sil silhouette_score(X_scaled, labels, sample_size300, random_state42) silhouette_scores.append(sil) print(fk{k}, SSE{km.inertia_:.1f}, 轮廓系数{sil:.3f}) # 绘制肘部图 plt.figure(figsize(8, 4)) plt.plot(k_range, sse, markero) plt.xlabel(k) plt.ylabel(SSE) plt.title(Elbow Method for Optimal k) plt.show()参数说明n_init10表示KMeans从10个随机初始质心开始跑取SSE最小的结果。这个参数直接决定聚类稳定性演示时用10够用如果数据量大可以降到5追求稳定就提到20。initk-means是sklearn默认的初始化策略让初始质心尽量分开能显著降低陷入局部最优的概率。除非你有特殊需求否则不要改成random。sample_size300是轮廓系数的抽样计算参数因为轮廓系数的计算复杂度是O(n²)900条数据全算也就0.8秒演示无所谓但如果是10万条数据就必须抽样。观察输出结果SSE曲线在第3到第5个点之间会出现拐弯这是肘部。轮廓系数越大越好但要注意宿舍分配场景下轮廓系数超过0.4就很好追求0.6以上往往意味着簇数太多导致每簇人数过少。如果肘部不明显这种情况经常出现就需要回到业务去定k宿舍分配场景里一个簇的最小可用人数建议是宿舍容量的10倍。6人间宿舍k5时每簇平均180人远远够用k12时每簇平均75人就有点紧了。3.3 核心聚类与宿舍组队从簇标签到最终床位名单聚类本身只是打标签真正的分配逻辑在聚类之后。下面这段代码是完整流程里最核心的“落地段”# 选定k5重新训练一次得到簇标签 final_k 5 km KMeans(n_clustersfinal_k, initk-means, n_init15, random_state42, max_iter300) labels km.fit_predict(X_scaled) # 把簇标签放回原始数据表 data[cluster] labels # 宿舍容量以4人间为例 room_capacity 4 # 每个簇内随机打乱顺序再按4人一组切分 # 为什么不直接按簇分配宿舍 # 因为同簇内的人特征最相似从簇内取人组宿舍宿友间距离最小 result_rooms [] # 用于保存未能整除的剩余学生留给最后跨簇分配 remaining_students [] for cluster_id in range(final_k): cluster_data data[data[cluster] cluster_id].copy() # 打乱顺序避免按问卷提交时间聚类带来的顺序偏差 cluster_data cluster_data.sample(frac1, random_state42) n_cluster len(cluster_data) n_full_rooms n_cluster // room_capacity n_remaining n_cluster % room_capacity for i in range(n_full_rooms): room cluster_data.iloc[i * room_capacity:(i 1) * room_capacity] result_rooms.append(room.index.tolist()) # 剩下的人暂存之后统一处理 remaining_students.extend(cluster_data.iloc[n_full_rooms * room_capacity:].index.tolist()) print(f已分配完整宿舍 {len(result_rooms)} 间) print(f剩余待分配人数 {len(remaining_students)} 人)逻辑说明每个簇内人数是180人900人/5簇分成45间4人宿舍刚好整除。但真实数据不会这么巧一般总会余下1到3个人。这些剩人如果直接塞进其他簇的宿舍就破坏了“同类相聚”的原则。我的做法是剩下的学生先按簇聚集然后计算每个剩余学生到其他簇质心的距离放入距离最近的簇再执行一次组队。这样比随机塞入要好因为至少保证新室友在特征距离上是最近的。# 处理剩余学生按距离最近簇归队再组队 if remaining_students: # 取出剩余学生的特征 X_remaining scaler.transform(data.loc[remaining_students, feature_cols]) * weights # 计算到每个簇质心的距离 dist km.transform(X_remaining) # shape (n_remaining, k) # 每个剩余学生归属的最近簇 nearest_cluster dist.argmin(axis1) # 按最近簇重新分组 for sid, cid in zip(remaining_students, nearest_cluster): data.loc[sid, cluster_final] cid # 最后按簇归组切分宿舍 for cluster_id in range(final_k): cluster_students data[data[cluster_final] cluster_id].index.tolist() # 再次打乱并组宿舍 np.random.shuffle(cluster_students) for i in range(0, len(cluster_students), room_capacity): room cluster_students[i:i room_capacity] if len(room) room_capacity: result_rooms.append(room) else: # 最后一间不足4人时这条split记录留给人工微调 print(f提示:最后一间宿舍仅{len(room)}人: {room})参数说明km.transform(X_remaining)返回每个样本到所有簇质心的距离矩阵argmin找到最近簇。这个方法比重新调用predict更直观也能拿到距离值用于后续排序。分配策略里“打乱再切片”必须要做。如果不打乱问卷数据是按班级整体提交的同一个班级的学生大概率相邻结果就是宿舍按班级扎堆而不是按特征相似度分配。3.4 导出分配结果CSV、Excel和可视化三件套最后的分配结果要能直接交给辅导员使用不能只是一堆打印语句。# 为每个宿舍编号 room_list [] for room_idx, students in enumerate(result_rooms, start1): for pos, student_id in enumerate(students, start1): room_list.append({ room_id: fA-{room_idx:03d}, bed_position: pos, student_id: student_id, cluster: data.loc[student_id, cluster] }) room_df pd.DataFrame(room_list) # 合并学生原始特征方便检查分配质量 room_df room_df.merge(data[feature_cols [cluster]].reset_index(), left_onstudent_id, right_onindex, howleft) room_df.to_csv(dormitory_assign_result.csv, indexFalse, encodingutf-8-sig) # 统计每个宿舍内部的“特征离散度” room_stats room_df.groupby(room_id).agg( sleep_time_std(sleep_time, std), smoke_sum(smoke, sum), cleanliness_mean(cleanliness, mean), cluster_majority(cluster, max) ).reset_index() # 找出宿舍内离散度最高的10间这往往是矛盾高发宿舍需要人工复核 high_risk_rooms room_stats.nlargest(10, sleep_time_std) print(特征离散度Top10宿舍(建议人工复核):, high_risk_rooms[[room_id, sleep_time_std]].to_string(indexFalse))代码说明encodingutf-8-sig是因为Excel打开CSV时默认按ANSI编码不加这个中文表头会乱码。这是把结果交付给辅导员时最常见的坑。sleep_time_std衡量每间宿舍内部作息时间的标准差数值越大内部作息越不一致调解优先级越高。这里的“离散度Top10”本质上是用聚类结果做质量反查一个理想的宿舍内部成员应该属于同一个簇特征标准差尽量小。如果Top10宿舍里有跨簇混住的情况那就要回到第3.2节重新检查k值是否偏小。4. 参数调优k值、特征权重、归一化方式对结果的影响边界4.1 k值的业务约束不只是分数说了算轮廓系数和肘部法则告诉你的是“统计意义上的最优”但宿舍分配有硬约束。最典型的就是宿舍容量和总人数的整除关系。如果k6时每簇150人而宿舍全是4人间150除以4余2每簇都会多出2人6个簇一共多出12人这12个人必须跨簇分配跨簇就意味着“不相似的人住在一起”。这种约束没法靠KMeans解决但可以通过调k缓解。我的经验是在肘部区域选k时优先选能让每簇人数尽量接近宿舍容量整数倍的那个k值。差一个人和差三个人调起来难度完全不同。另外还要考虑行政管理粒度。同一个辅导员管理的宿舍最好分布在同一批簇里方便管理。比如5个簇分别对应5个宿舍楼栋辅导员只需要知道自己分管楼栋覆盖哪几个簇。4.2 n_init和max_iter的边界什么时候该加什么时候加了没用n_init这个参数是用来缓解KMeans随机初始化问题的。但有个边界数据量只有几百条、特征维度不到10时n_init10基本就能找到不错的结果加到50纯粹浪费算力。但如果数据出现明显的“簇间不平衡”——一个簇有400人另一个只有50人——此时初始质心落点的影响变大建议把n_init提高到20或30。max_iter默认300次迭代足够。如果发现结果不收敛sklearn会打印警告大概率不是迭代不够而是数据有问题——比如某个特征是常量、或者特征之间存在完全共线性。先去查数据不要盲目加到1000。4.3 特征权重的调整方法用“宿舍投诉率”做闭环迭代特征权重怎么调很多演示源码里根本不讲但这恰恰是决定生产环境效果的关键。常见做法是用上一学年的真实结果做标定特征初始权重调整方向调整原因是否吸烟2.5上调到3.5或下调到1.5如果投诉集中在吸烟矛盾上调如果宿舍里全是烟民反而相安无事下调作息偏好1.0上调到1.5晚睡和早睡的矛盾是投诉率最高的访客频率1.0下调到0.5学生普遍反映访客问题相对次要年级0.8保持或下调跨年级混住可以带新生产生正向引导卫生习惯1.0上调到1.5如果保洁检查扣分多说明这个特征预测力强这里的核心是权重服务于业务目标不服务于聚类纯度。提前定好“宿舍矛盾投诉率”作为结果指标新学期的分配方案出来后跟踪一学期用真实的投诉数据反向调整特征权重。这比任何数学指标都可靠。4.4 归一化的另一个选择MinMaxScaler什么时候更合适Z-score标准化是默认选择但有个场景下我会换MinMaxScaler作息作息时间这个特征。如果学校有极少数学生填了“凌晨2点睡觉”编码26Z-score会把这一条数据拽得离群导致整列被压扁正常的22和23点反而区分度变小。用MinMaxScaler会把作息时间压缩到0到1区间离群值的影响被限制在区间端点。两种方式对比from sklearn.preprocessing import MinMaxScaler, StandardScaler # StandardScaler:受离群值影响列方差被拉大 # MinMaxScaler:受离群值影响其他值被压缩到小区间 # 在宿舍分配场景里离群值本身就很重要—— # 凌晨2点睡的学生不应该被“平均化”TA需要被单独识别 # 所以我的选择是离群值有业务意义时用StandardScaler # 离群值只是噪声时用MinMaxScaler从业务上讲凌晨2点睡的学生恰恰是最需要被聚类分开的群体他们不应该因为Z-score标准差变大而和其他学生混淆。所以宿舍分配里我几乎总是保留StandardScaler。5. 宿舍分配避坑指南六个最常见的翻车点5.1 现象聚类结果里出现空簇k设为8跑完发现第7个簇一个人都没有。聚类输出的质心数组里对应位置是NaN后续组队时直接报错。原因是数据分布不均匀某些区域密度过低KMeans的初始质心落入空白区域后在所有迭代中都没能吸引到足够近的点最终形成空簇。尤其当特征维度较高10维以上时数据在高维空间稀疏空簇概率显著上升。解决第一个办法是调低k值第二个办法是换用k-means初始化它会让初始质心尽量靠近数据点降低空簇概率第三个办法是给空簇做“复活”——用距离其他质心最远的样本点作为新质心重新迭代。在sklearn里最简单的是增加n_init多次初始化能缓解但无法根除。5.2 现象同一间宿舍出现两个吸烟学生宿舍长直接拒收聚类本身不产生“吸烟聚在一起”的结果反而是组队阶段的问题。特征向量里吸烟权重是2.5吸烟学生自然聚集在同一个簇内如果这个簇人数多组队时大概率出现4人里有两三个吸烟者。原因理论上“全部烟民住一起”不算错——如果这是你的目标。但现实里宿舍管理通常要求“每间宿舍吸烟人数不超过1人”这属于硬约束聚类无法直接表达。解决在簇内组队阶段加一条贪心约束——每次从簇内取人时优先取不吸烟的填充宿舍吸烟者作为“特殊标记”每间宿舍至多分配一人。这里需要注意硬性约束会降低聚类相似度所以最好在3.3节的组队代码里加一个过滤函数def assign_room_with_smoke_limit(cluster_students, room_capacity, max_smoker_per_room1): 在确保每间宿舍吸烟人数不超过阈值前提下组队。 贪心策略:先排不吸烟者再插入吸烟者到已有宿舍。 room_list [] current_room [] smoker_count 0 # 把吸烟者放后面处理 non_smokers [s for s in cluster_students if data.loc[s, smoke] 0] smokers [s for s in cluster_students if data.loc[s, smoke] 1] for student in non_smokers: if len(current_room) room_capacity: current_room.append(student) else: room_list.append(current_room) current_room [student] # 处理吸烟者只放进未满且没有吸烟者的宿舍 for student in smokers: if len(current_room) room_capacity and smoker_count max_smoker_per_room: current_room.append(student) smoker_count 1 else: room_list.append(current_room) current_room [student] smoker_count 1 if current_room: room_list.append(current_room) return room_list这个函数的代价是宿舍内部的整体特征相似度会被吸烟约束打破但管理规则优先于聚类距离这一点必须在方案设计阶段想清楚。5.3 现象每次运行结果都不一样演示视频没法录演示视频脚本每次录制时聚类结果都变分配表也变根本没法拍。原因很直接——没有固定随机种子。sklearn的KMeans有random_state参数数据生成有np.random.seed组队阶段还有一个sample()打乱顺序三处都需要固定。如果不固定前两次运行结果或许一致第三次因为系统熵源变化就开始变。解决方法是把每个涉及随机的环节都显式传入random_state42且示例代码里在脚本文件最开头加一行import numpy as np np.random.seed(42) import random random.seed(42)注意random库和numpy.random是两个独立的随机源都要设置。这是python源码演示里最常见的黑匣子新手经常栽在这里。5.4 现象聚类结果在训练集上轮廓系数很高但分配后投诉率反而上升过拟合行为表现轮廓系数0.55看起来很漂亮住进去一个月辅导员收到大量换宿舍申请。原因聚类的目标函数是“簇内距离最小化”但这个目标未必和“宿舍矛盾最少化”一致。举例来说两个学生都爱打游戏作息也接近聚类会把它们放入同一簇——但事实上两个都爱打游戏的人住一起可能天天开黑到凌晨反而影响同宿舍其他人。聚类捕捉的是相似性不是互补性。解决把“是否吸烟”这类特征强约束化处理之后还要在簇内组队时增加“互补筛选”——比如每间宿舍至少安排一个早睡早起的学生作为“锚点”。这一步不能省略它才是宿舍分配方案里真正体现业务经验的地方。5.5 现象新老学生混合分配时聚类结果被老生的行为特征带偏每年宿舍分配都涉及新生和老生。老生已经住了一年有固定的作息和卫生习惯而新生填问卷时往往“谦虚地”填了个平均分——卫生习惯填3分实际可能是1分或者5分。两批数据的可信度不一致。原因KMeans对输入数据的质量一视同仁不知道哪些样本的标签是”伪装过“的。结果就是老生成了一堆离群点新生全部挤在中间几个簇分配效果失衡。解决在特征表里加一列“数据置信度”数值为0和1参与距离计算。这样新生和老生的特征距离会因为置信度差异而被拉开从而形成“按批分配”的天然隔离避免数据污染。这个方法虽然简单但很少在演示源码中出现因为它不属于算法范畴属于数据治理范畴。5.6 现象聚类时输入的离散特征太多欧几里得距离失真问卷里大量字段是分类变量是否吸烟、是否打游戏、是否在宿舍学习。这些0/1特征和等距数值特征混在一起算距离会出现“某两个人其他特征全一样只有一个0/1特征不同却被认为是完全不同的两类人”的问题。原因欧几里得距离把每个维度的差值平方累加两个样本在一维上的差异是1十维上的总距离会被稀释。如果数据里有8个0/1字段那这8个字段加在一起的距离差可能约等于2个数量级字段的距离差权重自然失衡。解决第2.3节的权重方案必须区分对待。0/1特征的权重不要统一给1.0建议给0.5让它们在总距离中的贡献降下来。同时可以把多个相关性高的0/1字段合成为一个“生活方式指数”字段比如“是否打游戏”和“访客频率”合并为“宿舍活跃度”。这个合并操作在2.2节里已经演示过study_score的做法。6. 用轮廓系数和特征画像验证分配质量一个可复现的校验流程分配方案做出来了不能只靠“看起来还行”。这里给出一套我用于验收的流程保证交付时能说清楚“为什么这批宿舍矛盾会少”。第一步是整体验证算聚类模型的轮廓系数。轮廓系数大于0.3可以接受大于0.4说明聚类结构明显。如果低于0.25就要回去调特征和权重。第二步是逐簇画像这是判断簇是否具有业务意义的关键# 每个簇的特征均值输出簇画像 cluster_profile data.groupby(cluster)[feature_cols].mean() print(cluster_profile.round(2))输出结果示例我实际跑过的一组数据clustersleep_timesmokegame_freqvisitor_freq022.10.050.301.9123.40.451.102.8222.80.100.602.1324.00.301.403.2423.10.150.802.4每个簇的特征均值构成了一幅清晰的画像。簇0是早睡、不吸烟、少访客的安静型簇1是吸烟比例高、游戏频率高的“夜猫群体”簇3则是典型的“嗨宿舍”。把这些画像单独拉出来给宿管看他们能直接分楼栋。第三步是宿舍间对比验证。我通常会抽取10%的宿舍逐个查看宿舍内成员来自哪个簇。如果一个宿舍内出现了3个不同簇的成员说明上面的组队逻辑有漏洞——很大概率是剩余学生跨簇分配造成的。这时回看第3.3节中remaining_students的处理逻辑检查是否因为延长了贪心分配而破坏了簇纯度。第四步是稳定性验证固定随机种子跑5遍对比每次分配的宿舍名单。如果一个学生在5次运行中分别被分到3个不同的簇说明该样本落在簇边界上这类学生就是未来调宿申请的高发群体建议提前人工干预。最后提一个细节所有验证脚本要在源码包里单独留一个validate.py文件。演示视频里很难直观展示“算法有效”但轮廓系数曲线和簇画像表格可以直接录到视频里让人一眼相信这套流程不是玄学。我自己的习惯是每次跑完数据都顺手把簇画像存成CSV一来用于汇报二来为下一学期调权重留底。宿舍分配这件事算法只解决了70%剩下的30%在特征设计和分配约束里。希望这篇拆解能帮你在自己的数据上少走弯路把KMeans真正用到可交付的程度而不是停在聚类出图这一步。本文还有配套的精品资源点击获取
网站建设高端定制企业官网