ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

KMeans聚类算法实现高校宿舍分配:特征工程、K值选择与后处理实践

KMeans聚类算法实现高校宿舍分配:特征工程、K值选择与后处理实践

简介:面向高校宿舍分配场景的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_clusters=K, init='k-means++', n_init=10, random_state=42)这几个参数要配套写。

from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # 先标准化,再聚类 scaler = StandardScaler() X_scaled = scaler.fit_transform(features) # n_clusters 由肘部法确定,init 用 k-means++,n_init 跑 10 次取最优 kmeans = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42) kmeans.fit(X_scaled) # 预测归类并写回 DataFrame features['cluster_label'] = kmeans.predict(X_scaled) features['distance_to_center'] = kmeans.transform(X_scaled).min(axis=1)

标准化这步容易被忽略,实际影响非常大。作息分数取值范围是 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['兴趣方向'], prefix='interest') features = pd.concat([features.drop(columns=['兴趣方向']), interests_dummies], axis=1)

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', encoding='utf-8') # 2. 特征工程 features = build_features(df) # 3. 标准化 + KMeans 聚类 scaler = StandardScaler() X_scaled = scaler.fit_transform(features) kmeans = KMeans(n_clusters=4, init='k-means++', n_init=10, random_state=42) labels = kmeans.fit_predict(X_scaled) # 4. 合并结果并输出 df['predicted_dorm_group'] = labels df.to_csv('最终聚类结果展示.csv', index=False, encoding='utf-8-sig') # 5. 可视化 plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap='viridis') plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1], marker='x', s=100) 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', index=False, encoding='utf-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 宿舍分配.py

requirements.txt的作用是锁定依赖版本。建议安装后顺手执行python -c "import sklearn; print(sklearn.__version__)"确认版本,如果和源码开发环境不一致,优先调整代码而不是强行装旧版本——比如用pip install scikit-learn==1.1.3可以回到更保守的版本,但新环境里直接改参数更省事。

4. K 值怎么定:肘部法则、轮廓系数与宿舍容量的三方博弈

4.1 肘部法则:看 SSE 曲线的拐点而不是拍脑袋

KMeans 的 K 值不能拍脑袋定。最常用的方法是肘部法则,它统计不同 K 值下所有样本到所属质心的距离平方和(SSE),K 从 1 逐渐增大时,SSE 会不断下降,但在某个点之后下降速度会明显变缓,这个点就是「肘部」,对应最合适的 K 值。

sse = [] K_range = range(2, 11) for k in K_range: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) km.fit(X_scaled) sse.append(km.inertia_) plt.plot(list(K_range), sse, marker='o') 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 值,经常出现 K=2 得分最高的情况,要和肘部法则交叉验证。

from sklearn.metrics import silhouette_score best_k, best_score = 2, -1 for k in range(2, 11): km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) labels = km.fit_predict(X_scaled) score = silhouette_score(X_scaled, labels) print(f'K={k}, silhouette={score:.4f}') if score > best_score: best_k, best_score = k, score print(f'best_k = {best_k}')

silhouette_score接收特征矩阵和聚类标签,不需要质心坐标。输出结果里如果 K=4 的得分是 0.52,K=5 是 0.48,选 4 没问题。但如果 K=2 得分 0.61、K=4 得分 0.52,不要直接选 2,先看下分布图,K=4 的簇边界是否更合理。

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() + 1

cumcount对每个簇内的人按顺序编号,// 4每 4 人得到同一个宿舍编号。这样每个簇内部被拆成多个 4 人间,床号room_bed是 1 到 4。这个思路解决了「KMeans 输出簇、实际要宿舍」的落差,但要注意最后一组可能不足 4 人,需要额外的合并逻辑,比如安排跟相邻宿舍混合。

5. 避坑与常见问题:聚类结果翻车现场与排错记录

5.1 现象:每次运行结果不一样

第一次跑出 A 类学生和 B 类学生同寝,第二次跑变成 A 和 C 同寝,模型结果不稳定。原因在于 KMeans 初始化质心是随机的,不同初始化可能收敛到不同局部最优解。解决方法是固定random_state,比如random_state=42,同时提高n_init=10,让算法跑 10 次取 SSE 最小的结果。如果你改random_state之后结果变了,这是正常的——同一个数据在不同随机种子下可能有多个合理划分,项目报告里说明了你选的种子即可。

5.2 现象:标准化前后聚类结果差异巨大

一开始直接用原始特征跑,聚类结果几乎等于按绩点区分,作息习惯完全不起作用。原因是绩点取值范围远大于作息分数,欧氏距离被绩点主导。解决方法是先把所有特征过StandardScaler,让每个维度方差为 1,再做聚类。这个坑几乎 80% 的 KMeans 初学项目都会踩,属正常的「聚类玄学」范畴——不信邪的话,把标准化前后的质心坐标打印出来对比,你会看到坐标尺度完全不同。

5.3 现象:CSV 用 Excel 打开中文列名乱码

最终聚类结果展示.csv用记事本打开正常,用 Excel 打开全是乱码。原因是 pandas 默认用utf-8保存 CSV,Excel 默认按 GBK 解析中文。解决方法是导出时指定encoding='utf-8-sig',utf-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_capacity=4): # 已有宿舍的缺口统计 dorm_room = df.groupby('dorm_id').size() deficit = dorm_capacity - dorm_room deficit = deficit[deficit > 0].sort_values(ascending=False) # 对混编池学生按距离最近的中心归并 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_capacity=4对应 4 人间。kmeans.transform算混编池学生到每个簇中心的距离,然后优先塞进缺口宿舍。

做完均衡分配后,我会做一次验证:计算每个宿舍内部学生的平均特征方差。方差小说明同宿舍的人特征一致,分配有效;方差大说明混编进来的人还是不合适。再跑一遍轮廓系数,如果 K=4 的轮廓系数在 0.4 以上,说明聚类结果可以接受,分配有据可依。

# 验证每个宿舍的内部一致性:特征方差越小越好 dorm_var = df.groupby('dorm_id')[feature_cols].var().mean(axis=1) print(f'平均宿舍内部方差: {dorm_var.mean():.4f}')

这个数值没有绝对标准,但同一个数据集里,方差均值如果从 1.2 降到 0.7,说明聚类起到了分组作用。从那以后我每次跑 KMeans 项目,都强制走一遍「标准化 → 肘部法选 K → 固定随机种子 → 容量均衡 → 方差验证」的流程,宁可多写几十行后处理代码,也不直接把聚类标签当最终分配结果。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表