肥胖风险因素分析这件事,很多人第一反应是"算个BMI不就行了"。但真做过健康数据分析的人都知道,BMI只是一个粗糙的截面指标,它既解释不了"为什么同样体重的人一个代谢正常一个已经胰岛素抵抗",也回答不了"哪几类人最容易同时踩中多个风险因素"。我这次做的项目,就是用K-Means聚类把人群先分成几类"画像",再用关联规则把风险因素之间的共现关系挖出来,最后用可视化把结论呈现给非技术背景的人看。整套流程从数据清洗到聚类、关联规则、图表输出,全部用Python跑通,源码结构清晰,适合做数据分析入门到进阶的实战参考,也适合健康管理、公共卫生方向的人拿来改造成自己的课题。
1. 先搞清楚这份数据到底能回答什么问题
1.1 肥胖风险分析的常见误区
很多人拿到一份健康问卷数据,第一件事就是算相关系数,然后发现"吃甜食"和"肥胖"的相关系数只有0.2出头,就觉得数据没用。问题出在哪?肥胖从来不是单一因素导致的,它是饮食、运动、睡眠、遗传、心理状态多个维度长期叠加的结果。用线性相关系数去衡量这种多因素耦合关系,本身就不合适。
我在项目初期也踩过这个坑。最开始想用逻辑回归直接预测"是否肥胖",结果模型AUC只有0.68,特征重要性排序也很平,看不出重点。后来换了个思路:先把人群按整体健康行为模式聚成几类,再在每一类内部看风险因素的组合规律。这个转变是整个项目的关键转折点,也是我建议所有做健康数据分析的人优先考虑的分析路径。
1.2 数据集字段与业务含义
这份数据来自公开的健康行为调查数据集,经过脱敏处理,包含约2000条个体记录。核心字段大致分为几组:
| 字段类别 | 代表字段 | 数据类型 | 业务含义 |
|---|---|---|---|
| 人口学 | Age, Gender, Height, Weight | 数值/类别 | 基础身份与体型信息 |
| 饮食习惯 | FAVC, FCVC, NCP, CAEC | 类别/数值 | 高频蔬菜、主餐数、两餐间进食 |
| 生活方式 | SMOKE, SCC, CALC, MTRANS | 类别 | 吸烟、热量监测、饮酒、交通方式 |
| 身体状态 | BMI, family_history | 数值/类别 | 体重指数、家族肥胖史 |
目标变量是NObeyesdad,把人群分成Insufficient_Weight、Normal_Weight、Overweight_Level_I、Overweight_Level_II、Obesity_Type_I、Obesity_Type_II、Obesity_Type_III七档。这个七分类设计比简单的"胖/不胖"二分类信息量大得多,也是后面聚类能出细粒度画像的基础。
1.3 为什么选K-Means而不是其他聚类
聚类算法一大堆,DBSCAN、层次聚类、GMM都能用,我最终选K-Means有三个现实理由。第一,数据经过标准化后各维度量纲统一,K-Means基于欧氏距离的假设成立;第二,K-Means的结果解释成本低,质心可以直接翻译成"这类人的典型特征";第三,数据量在2000条这个级别,K-Means的收敛速度和稳定性都很好,不需要调太多参数。
DBSCAN我试过,密度参数eps稍微变一点,聚类结果就剧烈波动,而且大量样本被划成噪声点,对后续画像分析不友好。GMM理论上更灵活,但需要假设每个簇服从高斯分布,健康行为数据明显不满足这个假设。所以K-Means是这个场景下的务实选择,不是因为它最先进,而是因为它最匹配当前数据特征和业务解释需求。
2. 数据预处理里那些不写进论文但必须做的事
2.1 缺失值与异常值的处理逻辑
原始数据里BMI字段有几条明显异常,比如身高1.6米体重30公斤算出BMI只有11.7,这种记录大概率是录入错误。我的处理方式是:先按BMI的3倍标准差原则标记异常,再人工核对原始记录,确认是错误后直接剔除,而不是用均值填充。健康数据里异常值往往携带真实信息,盲目填充会污染整个分布。
缺失值方面,CAEC(两餐间进食频率)有少量缺失,我用众数填充。这里有个细节:不要用全局众数,而是按NObeyesdad分组后取组内众数。因为不同体重档位的人,两餐间进食习惯本身就不同,用全局众数会把组间差异抹平。
# 分组众数填充示例 df['CAEC'] = df.groupby('NObeyesdad')['CAEC'].transform( lambda x: x.fillna(x.mode()[0] if not x.mode().empty else 'Sometimes') )2.2 类别变量的编码策略
数据里有大量有序类别变量,比如CAEC的取值是no、Sometimes、Frequently、Always,这是典型的有序变量。很多人直接上One-Hot编码,结果维度爆炸不说,还丢掉了顺序信息。我的做法是:有序变量用序数编码(no=0, Sometimes=1, Frequently=2, Always=3),无序变量(如MTRANS交通方式)才用One-Hot。
这个选择直接影响聚类结果。如果CAEC用One-Hot,K-Means在计算距离时会把"no"和"Always"当成完全无关的两个维度,而实际上它们在同一条轴上。序数编码保留了这种单调关系,聚类出的画像更符合直觉。
2.3 标准化:别小看这一步
K-Means对量纲极度敏感。Age范围是14到61,FCVC范围是1到3,如果不标准化,Age会主导整个距离计算,聚类结果基本就是按年龄分。我用的是StandardScaler做Z-score标准化,让每个维度均值为0方差为1。
注意:标准化参数必须只在训练集上fit,然后transform到全量数据。虽然这里是无监督学习没有严格意义上的训练测试划分,但如果你后续要做聚类结果的稳定性验证,这个习惯要养成。
3. K-Means聚类的参数选择与画像解读
3.1 确定K值:肘部法则加轮廓系数双验证
K值选多少,这是聚类项目里最容易被拍脑袋决定的一步。我用了两个指标交叉验证。肘部法则看SSE(簇内平方和)随K变化的拐点,轮廓系数看簇的分离度。实测下来,K=4时SSE下降明显放缓,轮廓系数也在K=4达到局部峰值0.31。
轮廓系数0.31不算高,但在健康行为这种高噪声数据里已经可以接受。我试过K=3和K=5,K=3时把Overweight和Obesity混在一起,区分度不够;K=5时多出来的那个簇只有几十个样本,没有业务解释价值。所以K=4是统计指标和业务可解释性的平衡点。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse, sil = [], [] for k in range(2, 9): km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(X_scaled) sse.append(km.inertia_) sil.append(silhouette_score(X_scaled, labels))3.2 四个簇的画像特征拆解
跑完K-Means后,我把每个簇的质心反标准化,还原成原始量纲,得到四类人群画像:
| 簇编号 | 样本占比 | 典型特征 | 风险等级 |
|---|---|---|---|
| Cluster 0 | 28% | 年轻、运动少、高频外食、BMI偏高 | 中高风险 |
| Cluster 1 | 35% | 饮食规律、有热量监测、BMI正常 | 低风险 |
| Cluster 2 | 22% | 家族肥胖史、两餐间进食频繁、BMI高 | 高风险 |
| Cluster 3 | 15% | 中年、吸烟、饮酒频繁、BMI中高 | 中风险 |
Cluster 2是我最关注的一类。他们的BMI均值达到32以上,家族史阳性比例超过80%,而且两餐间进食频率显著高于其他簇。这类人如果只靠"少吃多动"的通用建议,效果往往很差,因为他们的问题是多因素叠加的。
3.3 聚类结果的稳定性验证
聚类做完不能直接信,得验证稳定性。我的做法是:随机抽取80%样本重复聚类10次,看每次的簇划分和原始划分的ARI(调整兰德指数)。实测平均ARI在0.75以上,说明聚类结构比较稳定。如果ARI低于0.6,就要考虑是不是K值选错了,或者数据本身没有明显的簇结构。
另一个验证角度是看簇内样本的分布是否合理。Cluster 3只有15%的样本,但它的特征非常鲜明(吸烟加饮酒),这不是噪声簇,而是真实存在的一个亚群体。判断标准是:如果一个小簇在多个随机子样本中都能稳定出现,它就是有意义的。
4. 关联规则挖掘:找出风险因素的共现模式
4.1 Apriori算法的参数设定
关联规则用Apriori算法,核心参数是支持度、置信度和提升度。支持度设太低会挖出一堆偶然共现的规则,设太高又什么都挖不到。我经过几轮测试,最终定在最小支持度0.1、最小置信度0.6、最小提升度1.2。
提升度大于1.2意味着规则的前件和后件有正相关,不是随机共现。比如"家族肥胖史 -> 高频两餐间进食"这条规则,提升度1.8,说明有家族史的人出现高频进食的概率是无家族史人群的1.8倍,这个关联有实际意义。
from mlxtend.frequent_patterns import apriori, association_rules freq_items = apriori(df_encoded, min_support=0.1, use_colnames=True) rules = association_rules(freq_items, metric="confidence", min_threshold=0.6) rules = rules[rules['lift'] > 1.2].sort_values('lift', ascending=False)4.2 高价值规则的业务解读
挖出来的规则里,有几条特别值得说:
第一条:{家族肥胖史, 高频两餐间进食} -> {肥胖},支持度0.14,置信度0.78,提升度2.1。这条规则说明,有家族史且爱吃零食的人,肥胖概率极高。单独看家族史,置信度只有0.55;单独看高频进食,置信度0.48。两个因素叠加后置信度跳到0.78,这就是关联规则的价值——它捕捉到了交互效应。
第二条:{不运动, 常喝含糖饮料} -> {超重},支持度0.11,置信度0.71,提升度1.6。这条规则对应的是典型的"能量摄入高、消耗低"模式。
第三条比较反直觉:{有热量监测, 肥胖},置信度0.65。乍一看"监测热量的人反而胖"很荒谬,但结合业务一想就通了——很多人是因为已经胖了才开始监测热量,这是因果方向的问题。这条规则提醒我们,关联规则只能说明共现,不能说明因果,解读时必须结合业务背景。
4.3 关联规则与聚类结果的交叉验证
把关联规则的结果和聚类画像对照,会发现高度一致。Cluster 2(高风险簇)的典型特征正好对应第一条高提升度规则的前件组合。这种交叉验证很有价值:聚类从"样本相似性"角度分组,关联规则从"特征共现"角度找规律,两条路径指向同一个结论,说明发现是稳健的。
如果两者结论矛盾,比如聚类说A和B总在一起,但关联规则说A和B的提升度小于1,那就要回头检查数据编码或参数设置是不是有问题。
5. 可视化:让非技术背景的人也能看懂结论
5.1 聚类结果的降维可视化
高维聚类结果直接画不出来,我用PCA降到二维做散点图。虽然PCA会损失一部分信息,但用于展示簇的分离程度足够了。四个簇在二维平面上有明显边界,Cluster 2和其他簇的距离最远,视觉上就能看出这类人群的特殊性。
代码上用matplotlib加seaborn,每个簇用不同颜色,质心用大号标记标出。这里有个细节:散点图要加透明度(alpha=0.6),否则样本重叠严重时看不出密度分布。
5.2 风险因素热力图与雷达图
热力图用来展示各簇在关键特征上的均值差异。行是簇,列是特征,颜色深浅代表标准化后的均值高低。一眼就能看出Cluster 2在BMI、家族史、两餐间进食三个维度上颜色最深。
雷达图适合展示单个簇的多维特征轮廓。我把四个簇的雷达图并排放在一张画布上,每个轴是一个特征维度。Cluster 1的图形接近正多边形(各维度均衡),Cluster 2的图形在风险维度上明显外凸,这种视觉对比比表格更有冲击力。
5.3 关联规则的可视化呈现
关联规则用网络图展示最直观。节点是特征项,边是规则,边的粗细代表提升度大小。用networkx加matplotlib绘制,节点大小按支持度缩放。这样一张图能同时呈现几十条规则,比逐条列文字高效得多。
提示:网络图节点超过20个时会很乱,建议只展示提升度排名前15的规则,或者按后件分组分图展示。
6. 源码结构与复现要点
6.1 项目目录组织
源码按功能模块拆分,不是把所有代码堆在一个notebook里:
obesity_analysis/ ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── src/ │ ├── preprocess.py # 清洗、编码、标准化 │ ├── clustering.py # K-Means聚类与验证 │ ├── rules.py # 关联规则挖掘 │ └── visualize.py # 所有图表生成 ├── outputs/ │ ├── figures/ # 图表输出 │ └── reports/ # 分析报告 └── main.py # 主流程入口这样拆分的好处是每个模块可以独立测试。比如调聚类参数时,不用重新跑数据清洗,直接加载processed数据即可。
6.2 复现时最容易卡住的三个点
第一个坑是库版本。mlxtend的apriori函数在不同版本间API有变化,老版本用apriori(df, min_support=...)直接传DataFrame,新版本要求传入one-hot编码后的布尔矩阵。建议锁定mlxtend>=0.19.0。
第二个坑是中文显示。matplotlib默认字体不支持中文,图表标题会变成方框。需要在代码开头设置:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False第三个坑是K-Means的随机初始化。不设random_state的话,每次跑出来的簇编号可能不同,导致图表颜色对不上。所有涉及随机的步骤都要固定种子。
6.3 从分析到落地的延伸思路
这套分析框架不止能用于肥胖风险。把字段换掉,同样的流程可以套用到糖尿病风险、心血管风险等场景。核心逻辑是通用的:先聚类分群,再挖群内特征共现,最后可视化呈现。
如果要往产品化方向走,可以把聚类模型持久化(joblib保存),然后做一个简单的输入表单,用户填完问卷后实时输出所属风险群体和对应的风险因素提示。这个延伸不需要重构分析代码,只是加一层推理接口。
我在实际跑这个项目的过程中最大的体会是:数据分析的价值不在于算法多复杂,而在于你能不能把结论翻译成别人能听懂、能行动的语言。K-Means和Apriori都是教科书级别的老算法,但用对了场景、解读到位了,产出的洞察一点不比复杂模型差。另外提醒一句,健康类数据分析涉及个人隐私,即使用的是公开数据集,在展示结果时也要注意不要暴露任何可识别到个人的信息,这是做这类项目的基本底线。