简介:针对高校宿舍分配场景,基于K均值聚类算法的Python源码项目,面向数据挖掘学习者、开发者和高校信息化管理人员,演示如何用机器学习库完成学生特征聚类,将年龄、性别、专业、生活习惯等多维数据纳入分析,从而优化住宿分组、减少生活习惯冲突。压缩包共13个文件,包含主程序、依赖清单、CSV聚类结果、Markdown说明文档、XML工程配置,以及两段MP4演示视频,整体约10.71MB,结构清晰,便于按需查阅,已有819人学习。资源完整展示了从数据预处理、特征标准化、模型训练到结果可视化的项目流程,并讨论K值选择方法(肘部法则、轮廓系数)及KMeans++初始化策略。附带的CSV文件提供最终聚类结果和聚类中心占比,可直接用于分析;两段录屏分别演示程序运行与文档操作,配合源码注释,即使初学者也能快速复现并迁移到其他聚类场景,作为课程设计或宿舍分配系统的参考实现。
1. 宿舍分配不是排班问题,而是聚类问题
每年迎新季最头疼的不是床位不够,而是“怎么把一群人凑进同一间房”。手工按学号顺序排,或者按报到先后抽签,结果往往是夜猫子和早睡党同寝室,爱打游戏的和备考考研的共用一张桌子,不到一个月就有人申请调宿。另一个反直觉的点是:宿舍分配本质上不是分配问题,而是一个聚类问题。目标不是“给每个人找一个位置”,而是“让住在同一房间的人尽可能相似”。KMeans 聚类算法把每位学生的行为特征数字化后,按相似度聚成若干簇,每个簇对应一个宿舍的候选人群。这个思路对宿舍管理员和 IT 从业者都适用:前者拿到的是可理解的报表,后者看到的是标准 sklearn 流程如何在真实业务里落地。下面从算法原理开始拆。
2. KMeans 的距离世界:从欧氏距离到标准化输入
KMeans 的核心假设是:簇内样本距离尽可能小,簇间距离尽可能大。这里的“距离”默认是欧氏距离,但真实宿舍数据里有均值、方差差距极大的字段,直接扔进算法会让高量纲特征主导结果。所以这章先把距离和标准化讲清楚,再给最小可运行代码验证效果。
2.1 距离度量:为什么默认选欧氏距离
欧氏距离是 KMeans 的原生语言。算法在每次迭代中计算样本到质心的欧氏距离,把样本归到最近质心,然后重新计算质心坐标。这个过程收敛性稳定,计算成本低,在特征独立时效果最直接。相比曼哈顿距离和余弦相似度,欧氏距离对特征之间相关性有一定容忍度,但量纲差异会直接污染距离值,这是宿舍分配场景里最大的坑。
| 距离 | 公式(两个向量 x, y) | 适用场景 | 宿舍分配中的注意点 |
|---|---|---|---|
| 欧氏距离 | sqrt(sum((x_i-y_i)^2)) | 连续数值特征,如作息时间、自习时长 | 必须先标准化,否则量纲大的特征权重失控 |
| 曼哈顿距离 | sum(abs(x_i-y_i)) | 稀疏或整数特征,如卫生评分 | 对异常值更鲁棒,但 sklearn KMeans 默认不支持 |
| 余弦相似度 | 1 - x·y/( | x |
宿舍数据里,就寝时间、学习时长、晚归次数都是绝对数值,方向意义不大,所以默认用欧氏距离。后面的特征工程和代码也都围绕它展开。
2.2 标准化:不标准化等于只用一个特征
一个常见错误是:直接把“每日学习时长(0.5~10小时)”和“晚归次数(0~30次)”丢进 KMeans。晚归次数数值大,会在距离计算里碾压学习时长,聚类结果基本相当于按晚归次数单维度排序。解决办法是标准化,常见做法是 Z-Score 或 MinMaxScaler。Z-Score 更适合有正态趋势的连续值,MinMax 保留原始分布形状,适合成绩这种有明确边界的分数。
下面是最小可运行示例,用随机数据走一遍标准 KMeans 流程:
import numpy as np import pandas as pd from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 模拟 60 个学生,特征是就寝时间和自习时长 np.random.seed(42) data = pd.DataFrame({ "sleep_time": np.random.normal(23, 1, 60), # 就寝时间,均值 23 点 "study_hours": np.random.normal(5, 2, 60), # 每天自习小时数 }) scaler = StandardScaler() X_scaled = scaler.fit_transform(data) model = KMeans(n_clusters=4, n_init=10, random_state=0) model.fit(X_scaled) data["cluster"] = model.labels_ print(data.groupby("cluster").mean())代码逻辑很简单:先用 StandardScaler 做 Z-Score 标准化,再把标准化后的特征矩阵传给 KMeans。n_clusters=4表示聚成 4 个簇,n_init=10让算法用 10 个不同初始化质心各跑一遍,再选收敛结果最好的那次,避免随机初始化带来的局部最优。random_state=0让结果可复现,在排宿舍时需要保证每次运行输出一致。
输出groupby("cluster").mean()能看到每个簇的作息和自习均值,比如 0 号簇平均 23:30 睡觉、自习 3 小时,3 号簇平均 1 点睡觉、自习 8 小时。这个矩阵就是后面分配宿舍的中间依据。没做标准化时,study_hours取值范围小,sleep_time离散程度大,簇边界就会偏移,验证时经常出现“两簇学生学习习惯完全不同”的假聚类。
2.3 质心初始化与迭代次数的影响
真正影响结果的是初始化方式。sklearn 新版默认init="k-means++",让初始质心互相较远,比完全随机初始化稳定得多。max_iter=300是默认值,宿舍数据规模在几百到几千人,几十次迭代就收敛了,一般不用调。但如果发现不同random_state下簇成员变化明显,多半是数据里存在离群值,或者 K 选得不合适。
极端离群值比如某个学生填了就寝时间 3:00,标准化后这个点会变成七八个标准差的离群点,吸引一个质心单成一簇,宿舍里就只分到一个人。处理方法是特征清洗阶段直接剔除答卷异常值,或者标准化后手动 clip 到合理区间,不要留给聚类算法处理。宿舍分配讲究的是“尽量相似”,不是“孤立异常”,离群点应该单独走人工分配通道。
3. 宿舍分配的特征工程与 K 值选择
聚类算法吃的是数字,但宿舍场景里的原始信息是问卷和考勤记录。这一章解决两个问题:把学生画像转成数值矩阵,以及选多少个簇才匹配宿舍楼实际床位。
3.1 特征编码:把性格和习惯变成数值
宿舍分配关心的核心维度是作息、学习、卫生和社交。设计特征时不要贪多,特征越少越可控,每加一个特征就要确认它对聚类结果有区分度。下面是宿舍项目常用的一套特征模板:
| 特征名 | 原始形式 | 编码方式 | 说明 |
|---|---|---|---|
| sleep_time | 就寝时间“23:30” | 换算成小时小数 23.5 | 连续值,标准化后参与距离计算 |
| wake_time | 起床时间“07:00” | 换算成 23 + 7 = 30 再归一化 | 避免跨零点问题,睡眠类特征建议线性变换 |
| study_hours | 自习时长 0~10 小时 | 直接整型 | 连续值,反映勤奋程度 |
| hygiene_score | 1~5 分 | 整型 | 数量级较小,但标准化后无影响 |
| game_freq | 每周打游戏次数 | 0~5 定序 | 定序特征转整型,不要 one-hot,避免稀疏 |
| music_type | 安静/偏噪/摇滚 | 0/1/2 三值 | 类别少时直接编号,类别多时用 one-hot |
一个容易踩的坑是两个与时间相关的特征。如果就寝时间直接用“23:30”字符串,或者把 0:30 理解成 0.5 而不是 24.5,聚类时就会把“凌晨睡”和“傍晚睡”混在一起。我一般统一把时间转换成自正午 12:00 起的连续小时数,例如 23:30 转成 23.5,00:30 转成 24.5,这样距离是连续的,符合人的直觉。
特征标准化和前面一样用 StandardScaler 或 MinMaxScaler。宿舍数据通常规模小,几百条样本,不需要太复杂的降维,把特征控制在一双手数得过来的范围内即可。
3.2 肘部法则:用簇内平方和选 K
K 值对应的是“想分成多少类人”,但在宿舍场景里,K 更像是“先分成几类人,再把每类人里的个体平铺到各宿舍”。一般步骤是先跑肘部法则确定拐点,再用宿舍总容量反推 K 的下限,最终取一个既能解释又能操作的数。
下面代码用上一章的模拟数据画肘部图:
import matplotlib.pyplot as plt from sklearn.cluster import KMeans inertia = [] K_range = range(2, 11) for k in K_range: km = KMeans(n_clusters=k, n_init=10, random_state=0) km.fit(X_scaled) inertia.append(km.inertia_) # 簇内平方和,越小越紧凑 plt.plot(K_range, inertia, marker="o") plt.xlabel("K") plt.ylabel("Inertia") plt.show()inertia_是每个样本到所属质心距离的平方和。当 K 增加时,惯性指数下降,下降幅度从某个 K 开始明显放缓,那个拐点就是信息量收益最大处。实际经验是:宿舍分配取拐点附近 + 数据业务约束共同决定,比如每栋楼有 10 层、每层 20 间四人房,那 K 至少要为 10 的整数倍或者与班级数对齐。
3.3 容量约束:聚类不等于分房
聚类结果是“几类人”,不是“几个宿舍”。假设一共 600 人、每间 4 人,先聚成 5 类,每一类有 120 人,再把这 120 人按某种规则分成 30 个四人组。这里的“按某种规则”通常是随机排列后按床位窗滑动,或者按某个次要特征(比如学号后两位)均匀打散,避免同班的扎堆或班长全部落在同一间。
但聚类本质不保证每个簇的人数正好是 4 的整数倍,所以需要在簇内做一次二次划分。常见做法是:
- 对每个簇内样本按重要特征(如睡眠时间)排序;
- 用洗牌策略对排序后的序列做分段,段内 4 人成舍;
- 如果簇人数余 2~3 人,就和另一个余员类似的簇合并后再分段。
这一段逻辑属于业务规则,直接写进源码里,通常叫assign_rooms。KMeans 只负责把“脾气相投”的人归到一起,真正落到门牌号需要单独的约束处理函数,这个在下一章代码里会完整出现。
4. 源码工程落地:从 Excel 到分房名单的完整流程
这章按实际项目结构走一遍。源码包里带演示视频,演示的就是下面这个流程从点击运行到输出宿舍名单的完整过程。工程一般分成data/、src/、output/三个目录,核心脚本只做四件事:读数据、清洗、聚类、按容量分房。
4.1 工程结构与依赖
项目根目录下通常包含:
dormitory-kmeans/ ├── data/ │ └── students.xlsx # 学生信息及特征问卷 ├── src/ │ ├── preprocess.py # 数据清洗与特征转换 │ ├── cluster.py # KMeans 聚类与 K 值选择 │ └── assign.py # 簇内二次分配生成宿舍名单 ├── output/ │ └── room_assign.csv # 最终分配结果 └── demo.mp4 # 演示视频依赖只有pandas、numpy、scikit-learn、openpyxl。如果是在课程设计环境,通常再补一个matplotlib画聚类散点图和惯性曲线。演示视频里一般会展示从命令行运行python main.py,到终端打印聚类中心,再到生成 CSV 的一系列输出,方便评审时快速确认程序确实跑通了。
4.2 核心实现:读数据、标准化、聚类
下面是一个可以直接扩展的主流程代码片段:
import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans def load_and_prepare(path): """读 Excel,选出聚类特征列并做清洗""" df = pd.read_excel(path, engine="openpyxl") # 剔除关键特征缺失或异常的行 df = df[pd.to_numeric(df["sleep_time"], errors="coerce").notna()] features = ["sleep_time", "wake_time", "study_hours", "hygiene_score", "game_freq"] for col in features: df[col] = pd.to_numeric(df[col], errors="coerce").fillna(df[col].median()) scaler = StandardScaler() X = scaler.fit_transform(df[features]) return df, X, scaler def run_clustering(df, X, k=6, seed=42): """执行 KMeans,并把簇标签写回原表""" model = KMeans(n_clusters=k, random_state=seed, n_init=10) labels = model.fit_predict(X) df["cluster"] = labels return df, modelload_and_prepare先读 Excel,然后强制把特征列转成数值,遇到缺失用中位数填充。这样躲开了“问卷提交人填了文本”的脏数据问题。run_clustering里fit_predict一步完成训练和打标签,簇号直接落回 DataFrame。
标准化器scaler必须保留下来,后面如果在演示界面里输入单个新学生的特征,需要调用scaler.transform才能得到同样尺度下的坐标,否则新样本会和原有数据不在同一空间。
4.3 簇内二次分配:让 4 人间的名单真正落定
聚类完成后要按宿舍容量拆分。以四人寝为例,做一份带规则的分房函数:
import numpy as np def assign_rooms(df, room_capacity=4): """在每个聚类簇内按特征均匀分段,生成宿舍号""" df = df.sort_values(["cluster", "sleep_time"]) # 簇内按睡眠时间排序 df["room_id"] = "" rooms = [] for cluster_id in df["cluster"].unique(): cluster_df = df[df["cluster"] == cluster_id].copy() n_stu = len(cluster_df) n_full = n_stu // room_capacity # 完整宿舍数 remainder = n_stu % room_capacity # 余数人数 for i in range(n_full): room_members = cluster_df.iloc[i*room_capacity:(i+1)*room_capacity] room_name = f"{cluster_id:02d}-{i+1:03d}" df.loc[room_members.index, "room_id"] = room_name rooms.append((room_name, room_members.index.tolist())) if remainder > 0: # 余数人员先标记,后续用独立逻辑并入相邻簇宿舍 room_name = f"{cluster_id:02d}-X" df.loc[cluster_df.iloc[n_full*room_capacity:].index, "room_id"] = room_name return df, rooms这个函数先按cluster和sleep_time排序,使每个簇内成员按作息时间首尾相接。接着基于room_capacity做滑窗分段:每 4 人一段,宿舍号由簇编号和段序号组成。remainder是凑不够一间的散客,标记成X后在主流程外另行合并。
为什么要在簇内按sleep_time排序再做滑窗?因为 KMeans 只保证同一个簇里的人整体相似,不保证同一簇里 12 点和 1 点睡觉的人一定分不开。按睡眠时间排序后,相邻 4 人之间的作息差异最小,这比随机抽样更容易让同宿舍的人步调一致。排序字段可以根据实际需求换成成绩排名或卫生分,逻辑相同。
4.4 输出结果字段与演示视频对照
最终 CSV 输出至少包含以下字段:
| 字段 | 来源 | 作用 |
|---|---|---|
| student_id | 原始 Excel | 学生主键 |
| cluster | KMeans 结果 | 社交/作息类型标识 |
| room_id | assign_rooms 结果 | 最终宿舍号 |
| sleep_time | 原始特征 | 供人工抽查排布是否合理 |
| study_hours | 原始特征 | 供人工抽查排布是否合理 |
演示视频里常见三个镜头:第一段拍 Excel 输入文件的表头,确认特征列和脚本读取的一致;第二段拍命令行运行main.py,展示训练日志和聚类中心;第三段打开room_assign.csv,现场按宿舍号筛选,核对人数。对照视频时最容易发现的问题是脚本里读的列名和实际 Excel 表头不一致,一个下划线或大小写差异就会让KeyError中断流程。
5. 验证聚类质量与调参的最后一公里
宿舍分配完成后,不能只看“分配出来没”,还要回答“分得合理吗”。这里给三个具体检查方法。
第一个是轮廓系数。计算每个样本到同簇其他样本的平均距离 a,到最近邻簇所有样本的平均距离 b,轮廓系数是 (b-a)/max(a,b)。范围在 [-1,1],越接近 1 说明同类紧凑、异类分离。用silhouette_score对所有 K 值做批量扫描,一般 K 在 4~8 之间时轮廓系数最高,超过 8 曲线开始下滑,那个位置就是合理上限。
from sklearn.metrics import silhouette_score scores = [] for k in range(2, 11): km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_scaled) scores.append(silhouette_score(X_scaled, labels)) print(list(zip(range(2, 11), scores)))第二个是检查宿舍人数均匀性。分配函数的余数逻辑容易出现“某个宿舍只有 2 人”的情况,这时需要在主流程里加一个校验:对所有room_id做value_counts(),找出不等于 4 的宿舍。如果数量超过总宿舍数的 5%,说明簇内二次分配规则有问题,应该把余数人员提前合并到其他簇再分,而不是全部留在 X 屋。我一般会把余数人员按最近簇质心重新归类,再对目标簇做扰动更新。
第三个是固定随机种子并保存模型参数。宿舍名单需要可追溯,去掉random_state后每次运行结果可能不同,这会让现场答辩时难以解释“为什么这次分到的宿舍和上次不一样”。把model.json保存 K 值、特征列、标准化均值、簇质心和簇内人数,复核时就能用KMeans(*params)完全复现。
最后一个技巧是把聚类中心输出成一张“宿舍风格表”。每个簇中心对应一种学生画像,比如 1 号簇“23 点睡、自习 6 小时、游戏频率 1”,8 号簇“0 点睡、自习 2 小时、游戏频率 5”。在分配结果文件里附带这张表,管理员一看就知道每层楼的住宿风格大致是什么样。关于离群学生,不要塞进任何簇,单独用NearestNeighbors找最近质心,再人工放入容量有余的宿舍,整个过程维持一个观点:聚类负责相似,规则负责治安。
本文还有配套的精品资源,点击获取