☰
深圳杯A题复盘:居民身体健康因素分析与建模避坑指南
2026/10/14 10:52:17 网站建设 项目流程

简介:本资源为2023年深圳杯数学建模A题「影响居民身体健康因素分析」的获奖作品文档,面向备战数学建模竞赛的高校学生及数据分析学习者。内容围绕居民饮食习惯、生活习惯与常见慢性病的相关性展开,涵盖方差分析、皮尔逊相关性分析、逻辑回归与K-Means聚类等方法的完整建模流程,并针对四个子问题给出求解思路与健康管理建议。压缩包内共1个docx文件,约633KB,为完整论文正文,含摘要、问题重述、模型假设、符号说明、模型建立与求解等标准章节,附录附有源码可供参考学习。目前已有312人学习下载。读者可借此掌握从数据预处理、描述性统计到分类建模的完整赛题方案,理解不同年龄、职业人群饮食差异的量化分析路径,并参考获奖论文的写作结构与论证逻辑,适合作为赛前研读与建模复现的实战范本。

1. 深圳杯A题复盘:居民身体健康因素分析到底在考什么

2023年深圳杯A题“影响居民身体健康因素分析”是一道典型的多因素归因建模题。它给出一份居民健康与生活方式调查数据,要求参赛者找出哪些变量真正影响身体健康,并给出可解释的结论。这道题当年拿奖的作品,几乎都不是靠堆模型复杂度赢的,而是靠数据清洗的细致程度和统计推断的严谨性。适合谁看?会一点 Python 或 R、做过回归或分类、但面对真实问卷数据经常翻车的人。我见过太多队伍一上来就随机森林、XGBoost 一通跑,特征重要性画出来交差,结果被评委一句“共线性怎么处理的”问住。这篇笔记就按我复现这类赛题的顺序,把选型、代码、参数和踩坑一次讲透,让你拿到类似数据能直接套。

2. 数据摸底与变量清洗:别急着建模,先搞清问卷在问什么

2.1 先做缺失模式分析,而不是无脑填均值

问卷数据的第一坑是缺失值。很多人上来df.fillna(df.mean()),这是血泪教训级别的错误。健康类问卷的缺失往往不是随机的——比如收入高的人更不愿填收入,老年人更可能漏填运动频率。先看缺失模式:

import pandas as pd import missingno as msno import matplotlib.pyplot as plt df = pd.read_csv("health_survey.csv", encoding="utf-8") # 缺失比例排序 miss = df.isnull().mean().sort_values(ascending=False) print(miss[miss > 0]) # 缺失相关性矩阵图,看缺失是否成块出现 msno.matrix(df) plt.show() # 按关键分组变量看缺失差异 print(df.groupby("age_group")["income"].apply(lambda x: x.isnull().mean()))

逻辑说明:isnull().mean()给出每列缺失率,超过 5% 的列要单独决策。msno.matrix能看出缺失是散点还是整块——整块缺失通常意味着某类人群系统性拒答,这时填均值会引入偏差。参数上,encoding="utf-8"是中文问卷 CSV 的常见编码,若报错换gbk。分组看缺失差异是关键一步:如果某年龄段的收入缺失率是其他组的 3 倍,就该考虑把“是否填报收入”本身作为一个特征,而不是硬填。

2.2 分类变量的编码陷阱:有序和无序必须分开处理

问卷里“学历”是有序的(小学<初中<高中<大学),“婚姻状况”是无序的。用pd.get_dummies一把梭会把有序变量打散,丢失大小关系;用LabelEncoder处理无序变量又会凭空造出虚假的顺序。正确做法是分开:

from sklearn.preprocessing import OrdinalEncoder # 有序变量:显式指定顺序 edu_order = [["小学及以下", "初中", "高中/中专", "大专", "本科", "硕士及以上"]] oe = OrdinalEncoder(categories=edu_order) df["education_ord"] = oe.fit_transform(df[["education"]]) # 无序变量:独热编码,drop_first 避免共线性 df = pd.get_dummies(df, columns=["marital_status", "smoking"], drop_first=True) # 检查编码后是否还有 object 列 print(df.select_dtypes(include="object").columns)

逻辑说明:OrdinalEncoder的categories参数必须手动传入顺序列表,否则它按字母序排,中文会乱。drop_first=True在独热编码时丢掉第一个类别作为基准,这是回归建模避免虚拟变量陷阱的标准操作。最后检查残留 object 列,防止漏网变量导致模型报错。参数上,学历顺序列表要和问卷选项文字完全一致,差一个字就会产生新类别。

2.3 异常值:用业务逻辑卡,不要用 3σ 一刀切

问卷里的“每日运动时长”出现 1440 分钟(一天总分钟数),这明显是填错。但用 3σ 法则会误杀正常的高运动量人群。我的做法是双阈值:业务上限 + 分位数辅助。

# 业务硬边界 df.loc[df["exercise_min"] > 600, "exercise_min"] = np.nan # 单日运动超10小时视为异常 # 分位数软边界,只对连续变量 q1, q3 = df["bmi"].quantile([0.25, 0.75]) iqr = q3 - q1 lower, upper = q1 - 3 * iqr, q3 + 3 * iqr df["bmi"] = df["bmi"].clip(lower, upper) print(df[["exercise_min", "bmi"]].describe())

逻辑说明:业务硬边界优先于统计边界,因为问卷场景下“不可能值”比“离群值”更常见。clip把极端值拉回边界而非删除,保留样本量。参数上,3 倍 IQR 比常用的 1.5 倍宽松,适合健康数据这种天然长尾分布。处理完必须describe()复查,确认没有负数和荒谬最大值。

3. 建模选型:为什么有序回归和随机森林要一起用

3.1 先明确因变量的类型,再决定模型族

这道题的因变量“身体健康”可能是自评等级(1-5),也可能是二分类(健康/不健康)。很多队伍没看清就直接上线性回归,结果预测出 3.7 这种没有意义的等级。自评等级是有序分类,应该用有序 Logistic 回归;如果是二分类,用二元 Logistic。随机森林则作为非线性补充,用来捕捉交互效应。

from statsmodels.miscmodels.ordinal_model import OrderedModel import numpy as np # 因变量:自评健康等级 1-5 y = df["health_level"].astype("category").cat.codes X = df[["age", "bmi", "exercise_min", "education_ord", "income"]] # 有序 Logistic 回归 model = OrderedModel(y, X, distr="logit") res = model.fit(method="bfgs") print(res.summary()) # 平行线检验:看比例优势假设是否成立 from scipy import stats # 简化做法:对比有序模型与多项 Logistic 的似然比

逻辑说明:OrderedModel来自 statsmodels,distr="logit"指定 logit 链接。fit(method="bfgs")是数值优化,若报错换"nm"。输出系数表示自变量每增加一单位,健康等级提升一个档位的对数几率。关键假设是“比例优势”——不同等级间的系数相同,需要用平行线检验验证,不成立就得换多项 Logistic 或偏比例模型。参数上,cat.codes把文字等级转成 0 开始的整数,顺序按字母序,所以要先手动排好类别顺序。

3.2 随机森林做特征重要性,但要看排列重要性而非默认值

随机森林的feature_importances_基于不纯度,对高基数变量有偏。我一般用排列重要性(permutation importance)交叉验证:

from sklearn.ensemble import RandomForestClassifier from sklearn.inspection import permutation_importance from sklearn.model_selection import train_test_split X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) rf = RandomForestClassifier(n_estimators=500, max_depth=8, min_samples_leaf=20, class_weight="balanced", random_state=42) rf.fit(X_tr, y_tr) perm = permutation_importance(rf, X_te, y_te, n_repeats=30, random_state=42) for i in perm.importances_mean.argsort()[::-1]: print(f"{X.columns[i]}: {perm.importances_mean[i]:.4f} ± {perm.importances_std[i]:.4f}")

逻辑说明:n_estimators=500保证重要性稳定,max_depth=8防止过拟合问卷噪声,min_samples_leaf=20让每叶至少有 20 人,避免小样本噪声。class_weight="balanced"处理健康等级不平衡。permutation_importance在测试集上打乱每个特征,看准确率掉多少,比默认重要性可靠。n_repeats=30是重复次数,越大越稳但越慢,30 是性价比拐点。输出里标准差大的特征说明重要性不稳定,别写进结论。

3.3 共线性诊断:VIF 超过 10 的变量必须处理

问卷里“年龄”和“慢性病数量”往往高度相关,直接放进回归会导致系数符号反转。建模前跑 VIF:

from statsmodels.stats.outliers_influence import variance_inflation_factor from statsmodels.tools.tools import add_constant X_const = add_constant(X) vif = pd.DataFrame({ "variable": X_const.columns, "VIF": [variance_inflation_factor(X_const.values, i) for i in range(X_const.shape[1])] }) print(vif.sort_values("VIF", ascending=False))

逻辑说明:add_constant加截距项,VIF 计算必须包含截距。VIF>10 表示该变量能被其他变量线性解释,考虑删除、合并或改用岭回归。参数上,variance_inflation_factor第二个参数是列索引,循环时从 0 开始。如果“年龄”和“慢性病”VIF 都高,我一般保留年龄,因为它在业务上更基础,把慢性病改成“是否有慢性病”二值变量降低共线性。

4. 避坑与排查:复现这道题最常见的 5 个翻车点

4.1 现象:模型准确率 95%,但混淆矩阵里少数类全错

原因:健康等级 4 和 5 的样本只占 5%,模型全预测成 1 也能拿高准确率。解决:看classification_report的 macro F1 和各类召回率,用class_weight="balanced"或 SMOTE 过采样,但问卷数据我优先调权重,因为 SMOTE 会造出不存在的“合成居民”。

4.2 现象:有序回归报“Maximum Likelihood optimization failed”

原因:自变量量纲差异太大,收入是万级,BMI 是十级,优化器不收敛。解决:建模前对所有连续变量做标准化(x - mean) / std,或者用StandardScaler。注意有序回归的系数解释要还原到原始量纲,标准化后系数表示“每增加一个标准差”。

4.3 现象:随机森林特征重要性里“ID 列”排前三

原因:ID 列虽然无业务含义,但可能和采样顺序、地区批次相关,间接泄露了因变量。解决:建模前drop掉所有 ID、时间戳、行号。检查方法:看重要性排名里有没有明显不该出现的列,有就删了重跑。

4.4 现象:交叉验证分数波动极大,从 0.6 到 0.9

原因:问卷样本量小,且没做分层抽样,某折里全是健康人。解决:StratifiedKFold(n_splits=5, shuffle=True, random_state=42),保证每折类别比例一致。如果样本量低于 500,用留一法或重复交叉验证,报告均值±标准差而非单次分数。

4.5 现象:结论写“运动越多越健康”,但系数是负的

原因:共线性导致系数符号反转,或者运动时长和年龄交互——年轻人运动多且健康,老年人运动少但健康,混在一起出现辛普森悖论。解决:分年龄段建模,或加入交互项exercise_min * age,再看主效应。别硬解释一个被混杂的系数。

5. 从获奖作品里偷师:把结论落到可验证的阈值上

获奖作品和陪跑作品最大的差距,不在模型,在结论的可操作性。评委想看到的是“每周运动 150 分钟以上的人群,自评健康等级显著高 0.8 档”这种能落地的数字,而不是“运动对健康有正向影响”。我复现时会做一件事:用有序回归的系数反推阈值。

import numpy as np # 假设有序回归系数:exercise_min 的 coef = 0.0021(标准化后) # 标准化前的标准差 std_exercise = 120 分钟 coef_raw = 0.0021 / 120 # 还原到每分钟 # 计算从等级3提升到等级4所需的运动增量 # logit 累积概率 P(Y>=4) = 1 / (1 + exp(-(theta4 - X*beta))) # 简化:找 beta * delta = 0.5(约提升半个 logit)对应的 delta delta_min = 0.5 / coef_raw print(f"约需增加 {delta_min:.0f} 分钟运动,健康等级提升一档的概率显著增加") # 用测试集验证:按阈值分组看实际健康等级分布 df["exercise_group"] = pd.cut(df["exercise_min"], bins=[0, 150, 300, 600, np.inf], labels=["<150", "150-300", "300-600", ">600"]) print(df.groupby("exercise_group")["health_level"].mean())

逻辑说明:coef_raw把标准化系数还原到原始分钟单位,这样阈值才有业务含义。delta_min是使 logit 变化 0.5 所需的运动增量,0.5 对应概率提升约 12%,是保守估计。最后按阈值分组看实际均值,如果单调递增,说明阈值合理;如果中间组塌陷,说明有交互效应没捕捉到。参数上,bins的切点参考了常见运动指南(150 分钟/周),但要注意问卷里的运动时长可能是“每天”还是“每周”,单位搞错整个结论就废了。

我自己的习惯是:任何模型结论,都要回到原始数据里按阈值分组看一眼均值。如果分组均值和模型预测方向不一致,信分组,因为模型可能过拟合了。这套流程跑下来,深圳杯A题这类问卷归因问题基本能覆盖 90% 的坑。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询