4、K-Means实战:使用Python(Scikit-learn)对空气质量数据进行聚类
好了,前面我们聊了不少理论。什么K-Means原理、肘部法则、轮廓系数……说实话,光看理论容易犯困。我当年学的时候也是这样,看完一堆公式,脑子里全是问号:“这东西到底怎么用?”
今天咱们就来点实在的。直接上代码,用Python的Scikit-learn库,对真实的空气质量数据做一次完整的聚类分析。你跟着我走一遍,保证能上手。
4.1 准备工作:数据长什么样?
我选的是北京某监测站的PM2.5、PM10、SO₂、NO₂、CO、O₃这六项指标。数据是小时级别的,大概有几千条记录。你想想看,这些数据里藏着什么模式?
嗯,先看一眼数据的前几行:
import pandas as pd
df = pd.read_csv('air_quality.csv')
print(df.head())
| PM2.5 | PM10 | SO₂ | NO₂ | CO | O₃ |
|---|---|---|---|---|---|
| 35 | 68 | 12 | 45 | 0.8 | 62 |
| 128 | 195 | 28 | 78 | 1.6 | 35 |
| 22 | 41 | 8 | 33 | 0.5 | 88 |
数据看起来挺规整的。但有个坑——不同指标的量纲差太多了。PM2.5是几十到几百,CO才零点几。如果不做标准化,聚类结果会被数值大的指标主导。我在项目中遇到过这种情况,当时没注意,结果分出来的簇全是按PM10的值排的,其他指标完全没起作用。
4.2 数据预处理:标准化 + 降维
我个人习惯先标准化,再考虑要不要降维。标准化很简单:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
df_scaled = scaler.fit_transform(df)
标准化之后,数据就变成了“公平竞争”的状态。每个特征的权重都一样了。
至于降维,我建议先做一次PCA看看。为什么?因为高维数据可视化很困难,你没法直观地看到聚类效果。降到2维或3维,就能画图了。
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
df_pca = pca.fit_transform(df_scaled)
print("解释方差比例:", pca.explained_variance_ratio_)
输出大概是这样的:
解释方差比例: [0.48, 0.22]
前两个主成分解释了70%的方差。嗯,够用了。剩下的30%信息虽然丢了,但为了可视化,这个代价可以接受。
4.3 确定K值:肘部法则 + 轮廓系数
K值怎么选?我一般两种方法都跑一遍,互相验证。
先看肘部法则:
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
inertias = []
for k in range(1, 11):
kmeans = KMeans(n_clusters=k, random_state=42)
kmeans.fit(df_scaled)
inertias.append(kmeans.inertia_)
plt.plot(range(1, 11), inertias, marker='o')
plt.xlabel('K值')
plt.ylabel('惯性(Inertia)')
plt.title('肘部法则')
plt.show()
画出来的图,你会发现K=3或K=4的时候,曲线开始变平。这就是“肘部”。
再看轮廓系数:
from sklearn.metrics import silhouette_score
sil_scores = []
for k in range(2, 11):
kmeans = KMeans(n_clusters=k, random_state=42)
labels = kmeans.fit_predict(df_scaled)
sil_scores.append(silhouette_score(df_scaled, labels))
plt.plot(range(2, 11), sil_scores, marker='s')
plt.xlabel('K值')
plt.ylabel('轮廓系数')
plt.title('轮廓系数分析')
plt.show()
轮廓系数越高越好。我跑出来的结果是K=3时轮廓系数最高,0.52左右。K=4时掉到了0.44。所以最终我选了K=3。
4.4 执行K-Means聚类
确定了K=3,接下来就是跑模型了:
kmeans = KMeans(n_clusters=3, random_state=42)
labels = kmeans.fit_predict(df_scaled)
# 把标签加回原始数据
df['cluster'] = labels
print(df['cluster'].value_counts())
输出:
cluster
0 1245
1 876
2 543
三个簇的样本量不一样。簇0最大,簇2最小。这说明什么?说明大部分时间空气质量是“一般”状态,极端污染或极端清洁的情况比较少。
4.5 可视化聚类结果
光看数字不够直观。咱们把聚类结果画在PCA降维后的二维平面上:
plt.figure(figsize=(10, 6))
scatter = plt.scatter(df_pca[:, 0], df_pca[:, 1], c=labels, cmap='viridis', alpha=0.6)
plt.colorbar(scatter)
plt.xlabel('主成分1')
plt.ylabel('主成分2')
plt.title('K-Means聚类结果(K=3)')
plt.show()
画出来的图,三个簇在空间上分得挺开的。簇0在中间偏左,簇1在右上角,簇2在右下角。边界清晰,说明聚类效果不错。
4.6 解读每个簇的含义
聚类不是终点,解读才是。我习惯把每个簇的均值算出来,看看它们各自代表什么“模式”。
cluster_means = df.groupby('cluster').mean()
print(cluster_means)
| 簇 | PM2.5 | PM10 | SO₂ | NO₂ | CO | O₃ |
|---|---|---|---|---|---|---|
| 0 | 45 | 82 | 15 | 50 | 0.9 | 55 |
| 1 | 135 | 210 | 32 | 85 | 1.8 | 30 |
| 2 | 18 | 35 | 6 | 28 | 0.4 | 78 |
看这张表,一目了然:
- 簇0:中等污染。各项指标都在中间水平。这是最常见的状态。
- 簇1:重度污染。PM2.5高达135,PM10到210,CO也高。但O₃很低。这说明什么?说明污染源主要是燃煤和机动车排放,不是光化学污染。
- 簇2:清洁空气。所有污染物都很低,O₃反而高。嗯,这符合常识——清洁天气阳光充足,臭氧容易生成。
我曾经帮一个环保局做项目,他们拿到聚类结果后特别惊讶:“原来我们这儿的污染模式就三种!”后来他们根据这个结果,针对不同模式制定了不同的应急措施。比如簇1出现时,重点查燃煤和柴油车;簇2出现时,反而要提醒臭氧敏感人群注意防护。
4.7 避坑指南
最后,分享几个我踩过的坑:
- 随机种子要固定。 K-Means的初始中心点是随机选的。不固定种子,每次跑出来的结果可能不一样。我习惯设
random_state=42。 - 不要直接对原始数据聚类。 量纲不同,结果会偏。标准化是必须的。
- K值不是越大越好。 有人觉得K越大,分得越细。但K太大,每个簇的样本太少,失去统计意义。而且过拟合了,换个数据集可能就不一样了。
- 聚类结果要结合业务解读。 光看数学指标没用。你得能解释“这个簇代表什么”。否则聚类就是一堆数字。
好了,这一章的内容就到这儿。代码你拿回去跑一遍,数据换成你自己的,看看能发现什么模式。嗯,动手试试吧。