KMeans聚类算法实现高校宿舍分配:特征工程、K值选择与后处理实践
发布时间:2026/10/25 13:56:32 锦皓数字建站

简介面向高校宿舍分配场景的KMeans聚类算法Python实现资源适合学习无监督学习、数据挖掘以及Python工程实战的开发者。源码基于scikit-learn实现配套演示视频与说明文档完整覆盖学生特征数据预处理、KMeans模型训练、聚类结果可视化以及K值选择评估等关键环节可直接运行并迁移到类似的分组优化问题中。压缩包共13个文件包括Python脚本、CSV聚类结果数据、Markdown使用说明、程序运行演示与文档演示MP4等整体约10.71MB目录结构简洁清晰。目前已有820人学习下载。借助完整源码与双录屏演示读者可直观理解算法原理与工程落地细节掌握从学生数据清洗到宿舍分组输出的完整流程尤其适合课程设计、毕业设计或实际宿舍管理场景参考借鉴。1. KMeans聚类算法与高校宿舍分配把学生当数据点而不是按学号硬分高校宿舍分配这个场景看着简单真做起来全是坑。按学号顺序排、按班级凑、按报到顺序塞这些办法都省事但结果就是生活习惯完全不同的人住在一起作息冲突、卫生矛盾不断辅导员隔三差五被叫去调解。KMeans聚类算法解决的是另一个思路把每个学生看成多维空间里的一个点维度是作息、运动、游戏习惯这些特征然后用无监督学习把这些点聚成K堆让同堆的人特征距离近、不同堆的距离远。这样一来宿舍分配从「行政安排」变成了「数据聚类」方向上是对的。这份源码包基于 Python 和 scikit-learn 实现从特征构建、数据预处理到聚类输出全部走通附带演示视频和两份 CSV 结果文件。适合本科课程设计、毕业设计也适合想用 KMeans 做资源分组但不想从零写代码的从业者。要说明的是KMeans 解决的是「怎么分组最合理」的问题不负责「床位够不够」的行政约束这部分得靠后处理兜底后面章节会讲。2. 宿舍分配的数据建模特征怎么选、怎么标准化、KMeans 原理与 sklearn 实现2.1 学生画像的数值化作息、运动、游戏习惯怎么变成坐标KMeans 算的是距离所以所有特征必须变成数值。原始数据里如果存的是「早起型」「夜猫型」这种文本模型不认识必须编码。常见的做法是映射成有序数值比如作息习惯1 表示 23 点前睡2 表示 0 点前后3 表示 1 点后运动习惯1 表示从不2 表示偶尔3 表示每周三次以上游戏习惯同理用频率等级表示。这里有个原则如果特征本身有顺序含义用有序编码如果是「喜欢篮球 / 喜欢足球 / 喜欢桌游」这种并列关系有序编码就有风险会人为制造远近关系改用 one-hot 更稳。# 把原始文本特征映射成数值 def build_features(raw_df): df raw_df.copy() sleep_map {23点前睡: 1, 0点前后: 2, 1点后: 3} sport_map {从不运动: 1, 偶尔运动: 2, 每周3次以上: 3} game_map {不玩游戏: 1, 偶尔玩: 2, 重度玩家: 3} df[sleep_score] df[作息习惯].map(sleep_map) df[sport_score] df[运动习惯].map(sport_map) df[game_score] df[游戏习惯].map(game_map) # 只保留数值特征列丢掉文本列 feature_cols [sleep_score, sport_score, game_score, 年级, 平均绩点] return df[feature_cols]这段代码的作用是把分散在文本列里的信息统一换算成数值坐标。map函数负责字典映射映射关系决定了特征的语义距离比如作息习惯里的 1 和 3 之间的距离是 2恰好对应「23 点前睡」和「1 点后睡」的差异程度。年级和平均绩点本身就是数值直接保留。注意这里没有把学号纳入特征——学号是标识符不是特征放进去只会污染距离计算。2.2 KMeans 迭代原理初始化质心、分配、更新、收敛KMeans 的核心逻辑分四步先随机选 K 个质心然后计算每个点到各质心的欧氏距离把点分给最近的质心接着用每个簇内点的均值更新质心位置重复分配和更新直到质心移动距离小于阈值或达到最大迭代次数。这个流程看着简单但对初始化非常敏感。sklearn 默认用的是k-means初始化它会让初始质心尽量分散比纯随机初始化收敛更快、结果更稳。这就是为什么源码里KMeans(n_clustersK, initk-means, n_init10, random_state42)这几个参数要配套写。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 先标准化再聚类 scaler StandardScaler() X_scaled scaler.fit_transform(features) # n_clusters 由肘部法确定init 用 k-meansn_init 跑 10 次取最优 kmeans KMeans(n_clusters4, initk-means, n_init10, random_state42) kmeans.fit(X_scaled) # 预测归类并写回 DataFrame features[cluster_label] kmeans.predict(X_scaled) features[distance_to_center] kmeans.transform(X_scaled).min(axis1)标准化这步容易被忽略实际影响非常大。作息分数取值范围是 1 到 3平均绩点可能是 0 到 5如果直接用原始值算欧氏距离绩点会把作息、运动这些维度完全压扁聚类结果基本等于按绩点排序。StandardScaler把每列变成均值 0、方差 1各维度对距离的贡献才平等。transform方法返回每个样本到所有质心的距离矩阵取min得到的是该样本到所属簇中心的距离这个值后面可以拿来筛查「边缘人」——离自己簇中心太远的学生说明特征不典型分到哪个宿舍都有点勉强。2.3 数据预处理的三个细节缺失值、标准化时机、类别特征编码方式数据预处理直接决定聚类质量这块翻车的概率比调参还高。第一个问题是缺失值学生问卷里经常有空项常见做法是均值填充或中位数填充年级这种有序数值用中位数更稳因为均值会被极端值拉偏。第二个是标准化时机必须是在填充缺失值和编码之后、聚类之前顺序反了等于白做。第三个是类别特征的编码方式前文说过并列关系的特征要用 one-hot不能用有序编码否则模型会认为「喜欢篮球」和「喜欢足球」的距离比「喜欢篮球」和「喜欢桌游」更近这显然不成立。# 缺失值处理与 one-hot 编码 features[年级] features[年级].fillna(features[年级].median()) # 兴趣方向 是并列类别用 one-hot interests_dummies pd.get_dummies(features[兴趣方向], prefixinterest) features pd.concat([features.drop(columns[兴趣方向]), interests_dummies], axis1)fillna用中位数填充年级字段get_dummies把「兴趣方向」拆成多个 0/1 列。这样处理后每个兴趣方向成为一个独立维度不存在人为的远近关系。做特征工程时记住一条KMeans 不挑特征但距离计算会放大所有特征的数值差异所以特征先拆干净再标准化比调参重要得多。3. 宿舍分配.py 源码拆解从数据加载到结果导出的完整主线3.1 主流程读数据、预处理、聚类、输出 CSV、可视化五步走源码宿舍分配.py的主流程分五个阶段对应README里的运行步骤。第一阶段读入学生数据源码里假设数据是 CSV 格式第二阶段做特征工程就是上一章那套映射和填充逻辑第三阶段标准化加聚类第四阶段把聚类标签合并回原始表算占比并导出两份 CSV第五阶段用 matplotlib 画散点图。整个流程没有多余的模块适合教学演示也适合改造成自己的数据。import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler if __name__ __main__: # 1. 读数据 df pd.read_csv(student_data.csv, encodingutf-8) # 2. 特征工程 features build_features(df) # 3. 标准化 KMeans 聚类 scaler StandardScaler() X_scaled scaler.fit_transform(features) kmeans KMeans(n_clusters4, initk-means, n_init10, random_state42) labels kmeans.fit_predict(X_scaled) # 4. 合并结果并输出 df[predicted_dorm_group] labels df.to_csv(最终聚类结果展示.csv, indexFalse, encodingutf-8-sig) # 5. 可视化 plt.scatter(X_scaled[:, 0], X_scaled[:, 1], clabels, cmapviridis) plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], markerx, s100) plt.show()fit_predict一步完成训练和预测返回每个样本的簇标签。cluster_centers_是质心坐标绘制成叉号标记。CSV 导出用utf-8-sig编码是为了让 Excel 打开不乱码纯utf-8在 Windows 上打开时中文列名经常变成乱码这是个血泪教训。3.2 两份 CSV 的生成逻辑结果明细和中心占比各有什么用压缩包里最终聚类结果展示.csv和聚类中心及占比展示.csv分别对应分配明细和统计摘要。明细表每行是一个学生的完整信息加predicted_dorm_group列直接可以拿去分宿舍。统计表记录每个簇的样本数量和占比以及各特征在簇中心的均值。占比信息用来判断分组是否均衡如果某个簇占比 60%说明 K 值没选好或者特征维度之间存在异常权重。# 统计每个簇的人数与占比 cluster_stats df.groupby(predicted_dorm_group).agg( 人数(student_id, count), 平均作息(sleep_score, mean), 平均运动(sport_score, mean), 平均绩点(平均绩点, mean) ).reset_index() cluster_stats[占比] cluster_stats[人数] / cluster_stats[人数].sum() cluster_stats.to_csv(聚类中心及占比展示.csv, indexFalse, encodingutf-8-sig)groupby按predicted_dorm_group分组分别统计人数和各项特征的均值。质心坐标对应的是标准化后的空间这列数据反映的是「这个簇的人大概长什么样」比如某个簇平均作息 2.8说明夜猫子占比高。查看占比表能快速判断是否需要调整 K 值或者增删特征不需要重新跑一遍程序直接用这份 CSV 做分析。3.3 演示视频对应的运行环境requirements.txt 与潜在版本坑源码包里requirements.txt列了核心依赖通常包含 numpy、pandas、scikit-learn、matplotlib。安装时有个坑需要注意新版的 scikit-learn 对某些参数的默认值有调整比如n_init在 1.2 版本前默认 10之后改成 auto。如果运行源码时看到KMeans相关的警告多半是版本不一致导致的。pip install -r requirements.txt python 宿舍分配.pyrequirements.txt的作用是锁定依赖版本。建议安装后顺手执行python -c import sklearn; print(sklearn.__version__)确认版本如果和源码开发环境不一致优先调整代码而不是强行装旧版本——比如用pip install scikit-learn1.1.3可以回到更保守的版本但新环境里直接改参数更省事。4. K 值怎么定肘部法则、轮廓系数与宿舍容量的三方博弈4.1 肘部法则看 SSE 曲线的拐点而不是拍脑袋KMeans 的 K 值不能拍脑袋定。最常用的方法是肘部法则它统计不同 K 值下所有样本到所属质心的距离平方和SSEK 从 1 逐渐增大时SSE 会不断下降但在某个点之后下降速度会明显变缓这个点就是「肘部」对应最合适的 K 值。sse [] K_range range(2, 11) for k in K_range: km KMeans(n_clustersk, initk-means, n_init10, random_state42) km.fit(X_scaled) sse.append(km.inertia_) plt.plot(list(K_range), sse, markero) plt.xlabel(K value) plt.ylabel(SSE) plt.title(Elbow Method) plt.show()inertia_就是 SSE 的值sklearn 直接算好存在模型属性里。做这个实验时对每个 K 值都要设置相同的random_state和n_init否则每次初始化不同SSE 曲线会抖动吃不准拐点位置。K 从 2 到 10 各跑一次K 等于 4 或 5 出现拐点的话就是合理的聚类数量。4.2 轮廓系数一个量化指标解决「肘部不明显」的问题肘部法则的痛点在于拐点不总是清晰。这时候用轮廓系数silhouette_score它综合了簇内紧密度和簇间分离度取值范围 -1 到 1越接近 1 说明聚类效果越好。实操时把 K 值从小到大跑一遍取轮廓系数最大的 K。但注意轮廓系数偏爱小的 K 值经常出现 K2 得分最高的情况要和肘部法则交叉验证。from sklearn.metrics import silhouette_score best_k, best_score 2, -1 for k in range(2, 11): km KMeans(n_clustersk, initk-means, n_init10, random_state42) labels km.fit_predict(X_scaled) score silhouette_score(X_scaled, labels) print(fK{k}, silhouette{score:.4f}) if score best_score: best_k, best_score k, score print(fbest_k {best_k})silhouette_score接收特征矩阵和聚类标签不需要质心坐标。输出结果里如果 K4 的得分是 0.52K5 是 0.48选 4 没问题。但如果 K2 得分 0.61、K4 得分 0.52不要直接选 2先看下分布图K4 的簇边界是否更合理。4.3 宿舍容量的硬约束KMeans 不管床位你得自己处理好均衡KMeans 只保证特征相近的人分到同一簇不保证每簇人数相等。现实里宿舍有固定床位数比如 4 人间宿舍正好容纳 4 人但 KMeans 跑出来一个簇 30 人、另一个簇 12 人这就没法直接安排。常见做法是先按 KMeans 聚类分组再在组内做均衡调整把人数多的簇拆开、人数少的簇合并。演示视频里假设一个宿舍 4 人最终结果表里的dorm_group只是一个分组编号具体床位分配还得靠后处理。# 按聚类结果分宿舍每 4 人一组 df[dorm_id] df.groupby(predicted_dorm_group).cumcount() // 4 df[room_bed] df.groupby([predicted_dorm_group, dorm_id]).cumcount() 1cumcount对每个簇内的人按顺序编号// 4每 4 人得到同一个宿舍编号。这样每个簇内部被拆成多个 4 人间床号room_bed是 1 到 4。这个思路解决了「KMeans 输出簇、实际要宿舍」的落差但要注意最后一组可能不足 4 人需要额外的合并逻辑比如安排跟相邻宿舍混合。5. 避坑与常见问题聚类结果翻车现场与排错记录5.1 现象每次运行结果不一样第一次跑出 A 类学生和 B 类学生同寝第二次跑变成 A 和 C 同寝模型结果不稳定。原因在于 KMeans 初始化质心是随机的不同初始化可能收敛到不同局部最优解。解决方法是固定random_state比如random_state42同时提高n_init10让算法跑 10 次取 SSE 最小的结果。如果你改random_state之后结果变了这是正常的——同一个数据在不同随机种子下可能有多个合理划分项目报告里说明了你选的种子即可。5.2 现象标准化前后聚类结果差异巨大一开始直接用原始特征跑聚类结果几乎等于按绩点区分作息习惯完全不起作用。原因是绩点取值范围远大于作息分数欧氏距离被绩点主导。解决方法是先把所有特征过StandardScaler让每个维度方差为 1再做聚类。这个坑几乎 80% 的 KMeans 初学项目都会踩属正常的「聚类玄学」范畴——不信邪的话把标准化前后的质心坐标打印出来对比你会看到坐标尺度完全不同。5.3 现象CSV 用 Excel 打开中文列名乱码最终聚类结果展示.csv用记事本打开正常用 Excel 打开全是乱码。原因是 pandas 默认用utf-8保存 CSVExcel 默认按 GBK 解析中文。解决方法是导出时指定encodingutf-8-sigutf-8-sig会在文件开头加 BOM 头Excel 识别后自动用 UTF-8 解析。项目源码里如果没有注意这点你改一行to_csv的参数就能解决。5.4 现象用自己的数据跑源码直接报 KeyError项目源码里写死了作息习惯、运动习惯、平均绩点这些列名换数据后找不到列。原因很简单源码是按演示数据设计的。解决方法有两个要么把新数据的列名改成源码里的名字要么改build_features函数里的字段映射把新列名对应进去。演示视频里的数据是固定的视频跑通不代表你的数据也能直接跑通这个必须有预期。5.5 现象聚类结果里有孤立的单人簇KMeans 跑完发现某个簇只有 1 个人其他簇都几十人。原因是数据里有极端离群点该学生所有特征都和别人差太远自己成了一个簇。解决方法是先做离群点筛查比如用distance_to_center排序把距离超过 95% 分位数的学生单独标记出来。实际分配时这种学生不适合按簇分宿舍要么人工安排要么增加特征维度让他能找到同类——但更常见的是接受这个结果人工插到某个习惯相近的宿舍里因为数据不能代表全部。6. 把聚类结果变成可落地的宿舍分配方案容量均衡的后处理技巧聚类只是中间产物宿舍分配要落到房间和床位。我一般在聚类后加一层容量均衡逻辑分成三步走。第一步计算每簇人数和目标宿舍容量的余数。第二步人数多的簇先拆成整间宿舍余下的人进「混编池」。第三步把混编池的人按特征距离重新归并到缺口宿舍优先选距离最近的簇。这个处理比直接按簇分更接近真实宿管需求。# 混编池归并按剩余床位缺口和特征距离匹配 def assign_leftover(leftover_df, dorm_capacity4): # 已有宿舍的缺口统计 dorm_room df.groupby(dorm_id).size() deficit dorm_capacity - dorm_room deficit deficit[deficit 0].sort_values(ascendingFalse) # 对混编池学生按距离最近的中心归并 assigned [] for _, student in leftover_df.iterrows(): center_dists kmeans.transform(scaler.transform( student[feature_cols].values.reshape(1, -1) ))[0] # 按簇中心距离排序优先填补缺口大的宿舍 candidate_groups np.argsort(center_dists) for g in candidate_groups: dorm_ids df.loc[(df[predicted_dorm_group] g) (df[dorm_id].isin(deficit.index)), dorm_id] if len(dorm_ids) 0: assigned.append((student[student_id], dorm_ids.iloc[0])) break return assigned这段代码的前提是df里已经有dorm_id和predicted_dorm_group。deficit是每个宿舍还缺几个床位dorm_capacity4对应 4 人间。kmeans.transform算混编池学生到每个簇中心的距离然后优先塞进缺口宿舍。做完均衡分配后我会做一次验证计算每个宿舍内部学生的平均特征方差。方差小说明同宿舍的人特征一致分配有效方差大说明混编进来的人还是不合适。再跑一遍轮廓系数如果 K4 的轮廓系数在 0.4 以上说明聚类结果可以接受分配有据可依。# 验证每个宿舍的内部一致性特征方差越小越好 dorm_var df.groupby(dorm_id)[feature_cols].var().mean(axis1) print(f平均宿舍内部方差: {dorm_var.mean():.4f})这个数值没有绝对标准但同一个数据集里方差均值如果从 1.2 降到 0.7说明聚类起到了分组作用。从那以后我每次跑 KMeans 项目都强制走一遍「标准化 → 肘部法选 K → 固定随机种子 → 容量均衡 → 方差验证」的流程宁可多写几十行后处理代码也不直接把聚类标签当最终分配结果。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。