基于KMeans聚类算法的高校宿舍分配:从开盲盒式分寝到可解释聚类分组
简介面向高校宿舍分配场景的K均值聚类算法Python源码与视频演示资料适合数据挖掘入门者、算法学习者及需完成宿舍分配项目的开发者。项目基于Python与sklearn库围绕学生年龄、性别、专业、生活习惯等特征构建数据集依次执行数值化转换、缺失值填充和标准化处理再利用K均值模型完成聚类分组。资源包含可运行的Python源码主程序以及聚类中心占比、最终聚类结果两份CSV结果数据便于核对分组质量使用说明和Markdown文档从算法原理、参数选择到运行步骤做了详细讲解两份MP4演示视频分别录下程序实际运行过程和文档操作即使零基础也能跟随上手。压缩包共13个文件涵盖Python源码、结果CSV、说明文档、演示视频及项目配置等整体约10.71MB结构清晰便于检索。已有820人浏览学习读者可借此复现宿舍分配实验并进一步理解肘部法则、轮廓系数、KMeans初始化等聚类优化方法为扩展更多个性化分寝策略打下基础。1. 基于KMeans聚类算法的高校宿舍分配从开盲盒式分寝到可解释的聚类分组每到开学季宿管办就被换寝申请淹没。早睡的和夜猫子住一间爱干净的和乱扔袜子的住一起矛盾从宿舍蔓延到班级群最后都变成辅导员的工作量。传统宿舍分配按学号或报到顺序机械分组结果全凭运气。基于KMeans聚类算法的高校宿舍分配本质是把每个学生的作息、性格、卫生习惯量化成数值特征用聚类算法把特征相似的人聚成一组再一组落到一间宿舍。它适合高校信息化管理人员、宿管老师和拿这个题目做课设毕设的学生。下面从特征设计、源码实现、参数调优到避坑把这条路完整走一遍。2. KMeans分宿舍的原理前提怎么把“合不合得来”变成可计算的向量KMeans本身是个黑匣子。你喂进去什么特征它就只能按什么特征分组。我第一次做这个项目时把“是否抽烟”“是否熬夜打游戏”“睡觉时间”“性格测试分数”一起丢进去聚类结果出来宿舍里全按“睡觉时间”分堆了——熬夜打游戏这个0/1特征在欧氏距离里几乎不起作用但“睡觉时间”的连续值跨度大反而主导了分组。这里有个关键认知宿舍分配聚类没有标准答案特征设计决定了结果上限KMeans只是把这个上限兑现出来。2.1 学生特征向量设计作息、性格、卫生习惯的量化口径我在实际项目里常用的特征集是四个维度作息节律起床时间和睡觉时间用连续值记录、卫生习惯打扫频率1-5等级、性格维度内向-外向1-5等级、生活习惯是否熬夜打游戏0/1二元。作息节律是宿舍冲突的第一大来源。起床时间用6.0到10.0的连续值睡觉时间用22.0到24.0的连续值。注意这里不要用“早睡”“晚睡”这种文字标签KMeans只认数值。卫生习惯和性格用李克特量表式的1-5分比如1分是“从不主动打扫”5分是“每天打扫且注重整洁”。这种量化方式问卷设计简单学生填起来也快。特征不是越多越好。加入过多弱相关特征比如“高考数学成绩”“是否喜欢某个明星”会让聚类结果被无关维度拉偏。一个判别标准每个特征必须能讲出一个“宿舍冲突故事”——睡觉时间差异大导致关灯冲突、打扫频率差异大导致卫生矛盾、性格差异大导致沟通问题。讲不出故事的特征直接删掉。2.2 从直觉到算法KMeans怎么把相似的人聚到一起KMeans的迭代过程分两步非常简单但常被误解。第一步随机初始化K个质心也就是K个宿舍“虚拟代表”第二步把每个学生分配到离他最近的质心所在的簇第三步每个簇重新计算质心也就是簇内所有学生的平均特征向量第四步重复第二步和第三步直到质心不再变化或变化小于阈值。这个过程中距离度量用的是欧氏距离也就是多维空间里的直线距离。宿舍分配场景里为什么欧氏距离够用因为这些特征都是连续数值或有序等级“1分”到“5分”的差就是4没有高维稀疏问题。如果你用了大量独热编码把专业、学院、生源地都展开成0/1列特征维度会膨胀到几十上百欧氏距离在高维空间会失去区分度——那时就该考虑余弦相似度或高斯混合模型GMM了。但在宿舍分配这个场景KMeans加欧氏距离就好参数少、速度快、结果易解释这也是它比GMM更常被选用的原因。2.3 特征标准化不做这步聚类结果等于没做这是KMeans项目里最常见的翻车点。直接把原始特征丢进KMeans“起床时间”的取值范围是6.0到10.0跨度4“性格分数”是1到5跨度也是4但分布形态完全不同。欧氏距离计算时起床时间几个小时的差异会完全淹没性格分数一两分的差异。所以你跑出来的聚类结果与其说是“综合画像分组”不如说是“按起床时间切了K刀”。解决方案是标准化。sklearn里有两种常用手段MinMaxScaler把特征压到[0,1]区间StandardScaler把特征转换成均值0、标准差1。我一般选MinMaxScaler原因是宿舍分配的特征里有0/1二元变量和1-5的等级变量MinMax会把它们都映射到[0,1]语义清晰——0就是完全不熬夜1就是天天熬夜。StandardScaler对正态分布数据效果更好但学生的作息习惯分布往往是双峰的早睡型和夜猫型用z-score反而会把这种双峰结构抹平。提示scaler需要用训练数据fit再用同一个scaler去transform后续的新生数据不能每次重新fit否则新老生的特征尺度不一致聚类结果没法复现。3. Python源码实现从模拟数据到宿舍分配名单的最小闭环网上免费的python源码大全里聚类相关的多半是Iris数据集跑个KMeans就完事。宿舍分配这个场景特殊在两点一是特征要自己设计二是聚类结果必须满足性别、学院这类硬约束。所以源码的核心不是调库而是把“约束修正”这件事写对。下面这套流程我用Python和sklearn完整跑通过先看最小闭环。3.1 最小可跑通的KMeans分配脚本数据生成到聚类输出代码先固定随机种子再生成模拟学生数据做标准化按性别和专业分组聚类最后输出分配预览。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler from sklearn.cluster import KMeans # 固定随机种子保证演示视频和源码复现一致 np.random.seed(42) # 生成200个模拟学生4个特征 # 起床时间6-10睡觉时间22-24卫生习惯1-5性格内向-外向1-5 n_students 200 df pd.DataFrame({ wake_time: np.random.uniform(6, 10, n_students).round(1), sleep_time: np.random.uniform(22, 24, n_students).round(1), hygiene: np.random.randint(1, 6, n_students), extrovert: np.random.randint(1, 6, n_students), }) # 给每个学生加上性别和专业用于后面的硬约束分组 df[gender] np.random.choice([M, F], n_students, p[0.5, 0.5]) df[major] np.random.choice([CS, EE, LAW, MED], n_students) # 标准化特征让四个特征在距离计算中权重均衡 scaler MinMaxScaler() X_scaled scaler.fit_transform(df[[wake_time, sleep_time, hygiene, extrovert]]) # 先按性别专业分组组内独立做KMeans df[cluster] -1 for (gender, major), group_idx in df.groupby([gender, major]).groups.items(): idx group_idx.to_numpy() if len(idx) 4: continue # 不足一间宿舍的人留到最后跨组二次分配 n_rooms len(idx) // 4 # 目标宿舍间数4人一间 km KMeans(n_clustersn_rooms, random_state42, n_init10) labels km.fit_predict(X_scaled[idx]) df.loc[idx, cluster] labels # 按性别、专业、簇排序生成分配预览 result df[df[cluster] 0].sort_values([gender, major, cluster]) print(result.head(16))这段代码的逻辑核心是用groupby([gender, major])把学生按性别和专业切分再在每组内部做KMeans。这样KMeans的聚类结果天然不会出现男女生混住一个簇的情况因为聚类根本没跨性别运行。参数说明np.random.seed(42)是关键中的关键。演示视频里跑出来一套宿舍名单源码复现时每次数据都得一样靠的就是这个种子。n_init10表示KMeans从10组随机初始化里选最优结果避免第一次初始化糟糕导致聚类跑偏sklearn较新版本默认行为类似但显式写出来更稳。random_state42同时传给KMeans保证算法内部的随机过程也可复现。n_rooms len(idx) // 4是用组内人数除以每间人数向下取整多出来不足4人的尾巴学生cluster保持-1留给后面的二次分配。3.2 K值怎么定固定宿舍规格与肘部法则的取舍K值在宿舍分配场景有两种确定方式。第一种宿舍规格是硬性的比如学校规定4人间那么“组内人数除以4”就是K不用纠结。第二种学校允许弹性安排比如4到6人间都可以这时用肘部法则在数据里找自然的簇数。聚类结果会更贴合学生真实分布但宿管排床位的复杂度会上升。肘部法则的代码很短核心是画出SSE随K变化的曲线import matplotlib.pyplot as plt from sklearn.cluster import KMeans # 计算不同K值下的簇内平方和SSE sse [] K_range range(2, 20) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10) km.fit(X_scaled) sse.append(km.inertia_) # inertia_即簇内平方和越小说明簇越紧凑 plt.plot(K_range, sse, o-) plt.xlabel(K) plt.ylabel(SSE) plt.title(Elbow Method for Optimal K) plt.show()逻辑说明随着K增大SSE必然下降因为簇越来越多、每个簇越来越小。真正的拐点在SSE下降速率骤减的地方那个K就是自然的簇数。但现实里拐点经常不明显这时候我会再看轮廓系数。轮廓系数的计算方式是对每个样本算它与同簇其他样本的平均距离a再算它与最近邻簇所有样本的平均距离b轮廓系数等于(b-a)/max(a,b)最终取全样本均值。范围[-1,1]越接近1越好。宿舍分配场景下特征本身就是人工设计的问卷评分噪声大是常态轮廓系数到0.25到0.35已经算合格不用追求0.5往上。如果肘部法则拐点在K8轮廓系数在K6和K8都差不多我选小的K因为宿舍数量越少、后续排床位越简单。3.3 硬约束修正性别、学院分组聚类与尾巴学生的贪心二次分配KMeans不认性别、学院它只认数值。宿舍分配如果没有约束修正算法会产出“早睡男生和晚睡女生住一间”这种宿舍办绝对不可能批准的名单。常见做法是先按性别加学院分组组内再做KMeans。这一步必须在标准化之后、聚类之前完成顺序反了会出现跨组特征尺度不一致的问题。每个分组整除后剩下的1到3个尾巴学生需要跨组拼宿舍。拼宿舍的原则不是随便塞而是把这些人的特征向量和待拼入宿舍的现有成员做距离计算。维护一个“未满宿舍列表”每个宿舍记录当前成员的平均特征向量新来的学生算一下自己和每个未满宿舍平均向量的欧氏距离选最近的。代码如下from scipy.spatial.distance import euclidean # 假设df里已经有cluster列cluster-1的是尾巴学生 tail df[df[cluster] -1].copy() unfilled df[df[cluster] 0].groupby([gender, major, cluster]) unfilled_list [] for (gender, major, cluster), sub in unfilled: if len(sub) 4: # 不足4人的簇也放进拼宿舍池 unfilled_list.append({ gender: gender, major: major, cluster: cluster, member_ids: sub.index.tolist(), centroid: sub[[wake_time, sleep_time, hygiene, extrovert]].mean().to_numpy() }) for _, stu in tail.iterrows(): vec stu[[wake_time, sleep_time, hygiene, extrovert]].to_numpy(dtypefloat) # 只允许同性别、同专业的学生拼进同一间宿舍 candidates [u for u in unfilled_list if u[gender] stu[gender] and u[major] stu[major]] if not candidates: continue # 实在凑不够时跨专业兜底的逻辑这里先省略 # 贪心选择质心距离最近的一个未满宿舍 closest min(candidates, keylambda u: euclidean(vec, u[centroid])) closest[member_ids].append(stu.name) closest[centroid] np.mean( df.loc[closest[member_ids], [wake_time, sleep_time, hygiene, extrovert]].to_numpy(), axis0 )逻辑说明这个片段的本质是贪心算法每来一个尾巴学生找当前所有未满宿舍中特征质心离他最近的塞进去。缺点是不是全局最优但宿舍分配场景里贪心已经够用实现简单、辅导员也好解释。参数说明euclidean来自scipy.spatial.distance等价于自己写np.linalg.norm(vec1 - vec2)。程序的硬过滤是同性别的同专业才允许拼宿舍实际项目里如果组内真的凑不满才允许跨专业合并但性别这条线绝对不能破。4. 从聚类结果到宿舍名单分配策略与结果验证KMeans输出的是“簇”不是“宿舍”。簇是特征空间里的点集合宿舍是物理房间里的床位集合两者之间还差一个落位步骤。这个步骤做不好前面的聚类等于白算。4.1 簇内落位大簇拆分、小簇合并的贪心策略聚类出来的簇大小不会精确等于4。实际分配时我做三层处理第一层正好4人的簇直接成一间第二层大于4人的簇按簇内成员到质心距离排序把特征最接近的人优先配对拆成多个4人组第三层小于4人的簇和分组余数学生汇总进拼宿舍池用贪心法补满。大簇拆分这一步很容易被忽略。同一个簇里可能同时存在“极早睡”和“稍早睡”让他们住一起没问题但如果你随手按顺序切4人份可能把特征最像的人拆开。所以要先算每个成员到簇质心的欧氏距离按距离排序后连续切段# 以大簇为例按成员到质心的距离排序后拆成4人组 def split_large_cluster(cluster_df, size4): # cluster_df包含该簇所有学生的特征列 feature_cols [wake_time, sleep_time, hygiene, extrovert] centroid cluster_df[feature_cols].mean().to_numpy() # 计算每个成员到质心的距离按距离排序 distances cluster_df[feature_cols].apply( lambda row: euclidean(row.to_numpy(dtypefloat), centroid), axis1 ) sorted_df cluster_df.loc[distances.sort_values().index] # 按每4人一段切分生成宿舍编号 groups [sorted_df.iloc[i:isize] for i in range(0, len(sorted_df), size)] return groups逻辑说明排序后连续切段保证每个4人组内部成员的特征差异最小。如果出现最后一段不足4人这段自动落入拼宿舍池交给贪心补充逻辑。排序是关键不排序直接切可能把离质心最近和最远的成员凑一起宿舍内部差异被无谓放大。参数说明size4是宿舍容量改成6就是6人间。实际项目中宿舍容量很少是固定的把size参数化后同一个函数可以支持不同宿舍楼栋的差异化配置。4.2 验证聚类质量轮廓系数、簇内距离和分配合理性检查聚类完成后不要急着出名单先算三个指标。第一个是轮廓系数衡量簇内紧密度和簇间分离度第二个是每个簇的平均内距也就是簇内成员到质心的平均欧氏距离第三个是离群点比例距离超过簇平均距离两倍的成员标记为离群点。from sklearn.metrics import silhouette_score # 全局轮廓系数 sil_score silhouette_score(X_scaled, df[cluster]) print(fSilhouette Score: {sil_score:.3f}) # 每个簇的平均内距与离群点检查 feature_cols [wake_time, sleep_time, hygiene, extrovert] for cluster_id in df[cluster].unique(): if cluster_id 0: continue sub df[df[cluster] cluster_id] centroid sub[feature_cols].mean().to_numpy() dists sub[feature_cols].apply( lambda row: euclidean(row.to_numpy(dtypefloat), centroid), axis1 ) avg_dist dists.mean() outliers (dists avg_dist * 2).sum() print(fCluster {cluster_id}: n{len(sub)}, avg_dist{avg_dist:.3f}, outliers{outliers})逻辑说明轮廓系数看的是整体分组质量我只把它作为参考不设死门槛。真正要盯的是离群点比例。宿舍分配场景下一个离群点就是一个“和谁都不合拍”的学生这种学生即使分进去后续换寝概率也高。发现离群点后我的处理方式是把ta重新放回拼宿舍池用4.1的贪心逻辑再做一次匹配。提示轮廓系数高不代表分配合理。如果特征集里混入了“高考成绩”这类弱相关列算法会按成绩聚类轮廓系数照样好看但宿舍矛盾该爆发还是爆发。所以做完聚类后强烈建议随抽几个簇打印成员特征用人的常识判断一下分组是否讲得通。4.3 可视化与名单导出聚类散点图和Excel分配表最后一步是把结果可视化并导出。可视化建议选两个最具区分度的特征做二维散点图给辅导员看的时候直观。导出用pandas的to_excel注意把同一宿舍的学生放在连续行方便打印张贴。import matplotlib.pyplot as plt # 二维投影图横轴起床时间纵轴睡觉时间 fig, ax plt.subplots(figsize(10, 6)) scatter ax.scatter( df[wake_time], df[sleep_time], cdf[cluster], cmaptab20, s50, alpha0.7 ) ax.set_xlabel(wake_time) ax.set_ylabel(sleep_time) ax.set_title(Student Clusters by Sleep Schedule) plt.colorbar(scatter, labelcluster id) plt.show() # 导出分配表每个学生一行标注宿舍号和床位 df[dorm_id] df[gender] df[major] - df[cluster].astype(str) df.sort_values([dorm_id, wake_time]).to_excel(dorm_assignment.xlsx, indexFalse)逻辑说明散点图只是辅助验证手段4个特征投影到2维必然丢失信息图上簇重叠不代表聚类失败。真正的交付物是Excel分配表dorm_id列把性别、专业、簇编号拼成宿舍号然后按宿舍号排序同一个宿舍的学生自然落在一起。注意导出前要检查dorm_id是否有重复。如果两个不同的簇拼出来一样的宿舍号说明聚类阶段就有问题——最常见的错误是分组聚类后没有重置簇编号导致不同专业组里都出现“cluster 0”。5. 宿舍分配聚类的避坑指南5个必踩的坑与排查方法这部分是血泪经验。从数据生成到名单导出每一环都有对应的坑按现象、原因、解决三个步骤说清楚。5.1 量纲没归一化聚类等于按单个特征切几刀现象聚类结果里学生几乎只按起床时间分组卫生习惯和性格完全没有参与。原因起床时间取值范围6到10跨度是4卫生习惯1到5跨度也是4但两个特征的方差结构完全不同。欧氏距离计算时起床时间的微小差异被放大性格、卫生这类低方差特征被淹没。KMeans本质是在做“最小化欧氏距离总和”哪个特征方差大哪个特征就主导分组。解决训练前统一做MinMaxScaler或StandardScaler。跑完标准化后打印一下各特征的标准差确认尺度在同一量级再进KMeans。如果某列标准差接近0说明这个特征几乎没有区分度直接删掉。5.2 KMeans强制分K簇但簇人数严重不均现象有的簇15人有的簇3人排宿舍时大簇拆分麻烦小簇又拼不满。原因KMeans只优化簇内平方和不做人数均衡。当学生特征分布本身集中时大量学生会被拉进同一个簇少数离群学生形成小簇这是算法特性不是bug。解决聚完先看每簇人数。大簇用4.1的排序切段拆小小簇进拼宿舍池。如果数据分布偏得厉害比如80%的学生作息都集中在同一区间考虑把宿舍容量约束提到6人间减轻均衡压力。必要时可以换DBSCAN但DBSCAN的eps参数调起来更费劲宿舍分配场景我更推荐KMeans加后处理。5.3 先全局聚类再按性别拆结果全乱现象全局聚类出来的簇成员特征确实相似但男女混在一起。按性别拆分后每个性别子集里的成员特征不再相似宿舍分配质量反而降了。原因KMeans不识别性别全局聚类得到的“作息相近”小组可能横跨性别。硬拆是按外部规则破坏聚类结构结果自然不伦不类。解决必须先按性别加学院分组组内独立做KMeans。这一步是流程顺序问题不是算法调参能解决的。很多课设源码翻车翻在这。5.4 演示视频和源码复现的结果对不上现象演示视频里宿舍A住的是甲乙丙丁自己跑源码出来变成了戊己庚辛甚至每次跑都不一样。原因源码没有固定随机种子。数据生成用的是np.random.uniform聚类初始化用KMeans默认随机跑一次一个结果。演示视频里录到的只是若干次随机里的一次。解决在数据生成前加np.random.seed(42)KMeans里传random_state42。注意数据生成的seed要放在所有随机调用之前如果把seed放在数据生成之后等于没固定。另外pandas的sample、train_test_split这类操作也要传random_state全链路都固定住复现才对得上。5.5 轮廓系数很高但实际分组不合理现象轮廓系数0.6结果辅导员看了一眼就说“这间宿舍三个人作息完全不一样”。原因特征集里混入了和宿舍冲突无关的弱相关特征比如高考成绩、身高、体重。算法确实按这些特征把学生分开了轮廓系数衡量的是“按这些特征分得好不好”不是“分得合不合理”。指标骗了你因为喂进去的数据本身有问题。解决特征设计阶段就删掉讲不出宿舍冲突故事的特征。聚类完成后抽检3到5个簇把簇内成员的特征值列出来人工核对。如果每个簇的作息、卫生、性格确实各自相似再信轮廓系数。6. 进阶把宿舍分配聚类做成可复用的分配服务前面五章解决了“跑通一次”的问题最后一章说怎么让它每个学期都能用。6.1 增量分配老生不动只聚类新生每学年的分配不是从零开始。老生宿舍已经稳定如果整体重新聚类今年的结果和去年完全不一致等于把所有人都拆散。做法是把老生宿舍视为固定簇每个宿舍的特征质心已经存在。新生单独做KMeans聚类后按距离匹配到未满的老宿舍质心用4.1那套欧氏距离判断。老宿舍不重新聚类新生的分组只和“宿舍质心”比较不和老生个体比较。6.2 生成可解释性分配报告辅导员要的不是质心坐标是“这间宿舍为什么这样分”的人话。做法是每间宿舍算特征均值自动生成一句话描述。比如“该宿舍成员作息相近睡眠时间均值23.2点性格偏内向卫生习惯良好”。这比给出一堆聚类编号强得多也减少了辅导员对算法的质疑。6.3 有效的验证方式回测换寝率最有说服力的验证是用上一届学生的问卷数据和真实换寝记录做回测。先跑一遍聚类分配得到模拟宿舍名单再去对比真实换寝名单里有多少人被分到了“特征差异很大”的宿舍。换个说法按聚类的标准看上一届换寝的人是不是普遍被分在不合适的宿舍里。如果是说明特征和算法方向对了如果换寝的人和室友特征差异并不大说明换寝主因不在作息和卫生要去补特征维度。我自己第一次做这个项目时没固定随机种子连续跑出三份不同的宿舍名单被辅导员当面质疑“这算法是不是扔骰子”。从那以后所有聚类项目先把seed写死、数据版本打上标签演示视频里录到什么样复现就必须是什么样。宿舍分配这事学生满意度很难量化但至少做到名单可复现、理由可解释宿管这边就愿意信你。希望帮到你。本文还有配套的精品资源点击获取