☰
基于Python的返乡发展人员预测:机器学习课程设计全流程实战
2026/10/12 10:04:45 网站建设 项目流程

简介:这份资源是面向高校机器学习课程设计与入门实践者的完整项目包,围绕“返乡发展人员预测”这一真实场景,用Python实现从数据处理到模型训练、预测输出的全流程,适合需要完成课程作业或想练手分类/回归任务的学生与开发者。压缩包共17个文件、约4.16MB,以csv训练与提交数据为主,辅以xml与iml工程配置、tsv训练日志、tfevents可视化记录及json参数文件,另有核心py脚本与gitignore,结构贴近实际竞赛与工程目录。项目覆盖特征提取、模型选择与调优、交叉验证等环节,可参考其数据划分与提交格式,快速复现baseline并理解调参思路。目前已有40人学习,虽规模不大,但作为课程设计范例,能帮助读者掌握Pandas、scikit-learn等库的落地用法,并积累劳动力流动预测的建模经验。

1. 返乡发展人员预测到底在预测什么:从一份课程设计说起

每年毕业季,总有一批机器学习课程设计卡在选题上——手写数字识别太烂大街,房价预测又找不到像样的数据。返乡发展人员预测这个题目之所以值得做,是因为它踩中了一个真实场景:地方在制定人才回流政策时,确实需要知道哪些人更可能回来、哪些人回来了又走。这不是拍脑袋能回答的问题,它需要人口属性、经济指标、就业意愿等多维特征一起建模。而用 Python 把这件事跑通,恰好覆盖了机器学习课程设计要求的完整链路:数据清洗、特征工程、模型训练、结果解释。如果你正在找一份能写进报告、又能真正跑出结果的课程设计,这个方向比调包跑 MNIST 有意思得多,也更容易在答辩时讲出业务价值。接下来我会按实际做项目的顺序,把每个环节拆开讲清楚。

2. 数据从哪来、特征怎么定:返乡预测的数据处理与标签设计

2.1 返乡预测的数据集长什么样

返乡发展人员预测的数据通常来自统计年鉴、地方人社部门公开数据或问卷调查。一份典型的训练数据会包含以下字段:年龄、性别、受教育程度、户籍类型、外出务工年限、当前务工地、月收入区间、家庭人口数、是否有子女在户籍地就读、本地是否有房产、本地产业岗位数量、返乡意愿标签。标签一般是二分类——愿意返乡 / 不愿意返乡,少数场景下会做成多分类(明确返乡 / 观望 / 不返乡)。

拿到原始数据后,第一件事不是急着喂模型,而是搞清楚每个字段的分布和缺失情况。我一般会先跑一段探索性分析,把数值型和分类型字段分开处理。下面这段代码是数据加载和初步探查的常用写法:

import pandas as pd import numpy as np # 加载训练数据,注意编码格式,统计年鉴导出的 CSV 常见 gbk df = pd.read_csv("return_migration.csv", encoding="gbk") # 基本信息:行列数、字段类型、缺失比例 print("数据规模:", df.shape) print("\n字段类型:\n", df.dtypes) print("\n缺失比例:\n", (df.isnull().sum() / len(df)).sort_values(ascending=False)) # 标签分布,判断是否类别不平衡 print("\n标签分布:\n", df["return_intention"].value_counts(normalize=True)) # 数值型字段描述统计 num_cols = ["age", "work_years", "monthly_income", "family_size"] print("\n数值字段统计:\n", df[num_cols].describe())

这段代码的逻辑很直接:先确认数据规模对不对,再看哪些字段缺失严重需要处理,最后检查标签是否严重不平衡。参数方面,encoding要根据实际文件调整,如果报UnicodeDecodeError就换utf-8或gb18030试。value_counts(normalize=True)输出的是比例而非计数,方便快速判断正负样本比。如果发现返乡意愿为“是”的样本只占 8%,那后面建模时就得考虑类别权重或过采样,否则模型会倾向于全部预测为“不返乡”,准确率看着高但毫无意义。

2.2 特征工程:哪些字段真正影响返乡决策

原始字段不能直接扔进模型。分类型变量要编码,数值型变量要考虑分箱或标准化,还有一些交叉特征值得构造。根据我做过的几个类似项目,以下特征处理方式比较稳妥:

原始字段处理方式理由
年龄分箱为 18-25 / 26-35 / 36-45 / 46+非线性关系,分箱后树模型更容易切分
受教育程度有序编码(小学=1 到 研究生=5)存在天然顺序
当前务工地独热编码或目标编码类别多,独热后维度膨胀
月收入区间保留序数编码区间本身有序
外出务工年限标准化 + 构造“年限/年龄”比值比值反映务工时间占人生比例
家庭人口数保留原始值 + 构造“劳动力占比”家庭负担影响返乡意愿

构造交叉特征时要注意别引入标签泄漏。比如“本地是否有合适岗位”这个字段,如果它是在知道对方是否返乡之后才填的,那就不能用。常见做法是只保留返乡决策发生之前就能获取的信息。

from sklearn.preprocessing import LabelEncoder, StandardScaler # 有序编码:受教育程度 edu_map = {"小学及以下": 1, "初中": 2, "高中/中专": 3, "大专/本科": 4, "研究生": 5} df["edu_level"] = df["education"].map(edu_map) # 构造比值特征 df["work_ratio"] = df["work_years"] / df["age"] df["labor_ratio"] = df["family_labor"] / df["family_size"] # 分类型字段独热编码,drop_first 避免共线性 df = pd.get_dummies(df, columns=["current_city", "industry"], drop_first=True) # 数值字段标准化,树模型可跳过,线性模型必须做 scaler = StandardScaler() df[["age", "work_years", "monthly_income"]] = scaler.fit_transform( df[["age", "work_years", "monthly_income"]] ) # 标签编码 le = LabelEncoder() df["label"] = le.fit_transform(df["return_intention"]) # 是=1, 否=0

这里有几个参数需要留意。drop_first=True是为了避免独热编码后的虚拟变量陷阱,在线性模型里很重要,树模型影响不大但也没坏处。StandardScaler对树模型不是必须的,但如果你打算同时对比逻辑回归和随机森林,统一标准化能省事。work_ratio这个特征在实际项目中多次被模型排到重要性前五,原因是它同时编码了年龄和务工时长两个维度的信息。

注意:如果数据里“当前务工地”类别超过 20 个,独热编码会让特征维度爆炸,这时候改用目标编码或频率编码更合适。目标编码要用交叉验证的方式计算,防止过拟合。

2.3 训练集测试集划分的讲究

课程设计里最常见的翻车点之一,就是随机划分后模型在测试集上表现很好,但一换数据就崩。返乡预测这类社会经济学数据往往有时间维度,比如 2020 年的数据和 2023 年的数据分布可能不一样。如果数据里有年份字段,正确做法是按时间切分——用早年份做训练,晚年份做测试。如果确实没有时间字段,那至少要用分层抽样保证训练集和测试集的标签比例一致。

from sklearn.model_selection import train_test_split # 分层抽样,保证正负样本比例一致 X = df.drop(columns=["return_intention", "label", "education"]) y = df["label"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print("训练集标签分布:\n", y_train.value_counts(normalize=True)) print("测试集标签分布:\n", y_test.value_counts(normalize=True))

stratify=y是关键参数,不加的话如果正样本本来就少,测试集里可能一个正样本都没有。random_state=42是为了结果可复现,课程设计报告里要写清楚这个值。test_size=0.2是常规选择,数据量少于 500 条时建议降到 0.15 或改用交叉验证。

3. 模型选型与训练:从逻辑回归到集成模型的完整代码路径

3.1 先跑一个基线模型:逻辑回归为什么不能跳过

很多人一上来就上 XGBoost,结果答辩时被问“你的基线是什么”就卡住了。逻辑回归在返乡预测里不是凑数的——它的系数可以直接解释为“某个特征每变化一个单位,返乡几率的变化”,这在政策分析场景里比一个黑箱模型的准确率更有说服力。而且逻辑回归训练快,能帮你快速判断特征工程有没有做对。

from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, roc_auc_score # 基线模型:逻辑回归,加 L2 正则防止过拟合 lr = LogisticRegression( penalty="l2", C=1.0, # 正则强度倒数,越小正则越强 class_weight="balanced", # 自动处理类别不平衡 max_iter=1000, random_state=42 ) lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) y_prob_lr = lr.predict_proba(X_test)[:, 1] print("逻辑回归分类报告:\n", classification_report(y_test, y_pred_lr)) print("AUC:", roc_auc_score(y_test, y_prob_lr)) # 查看系数,判断特征方向是否符合常识 coef_df = pd.DataFrame({"feature": X_train.columns, "coef": lr.coef_[0]}) print(coef_df.sort_values("coef", ascending=False).head(10))

class_weight="balanced"会自动按类别频率反比设置权重,比手动调参省事。C=1.0是默认值,如果发现训练集准确率远高于测试集,可以降到 0.1 或 0.01 增强正则。max_iter=1000是为了防止数据标准化后仍不收敛的警告。系数表里如果出现“月收入越高返乡意愿越强”这种反常识结果,大概率是特征编码方向搞反了,需要回头检查。

3.2 随机森林与梯度提升:参数怎么调、调什么

树模型在表格数据上通常比逻辑回归强,但参数不调好也白搭。随机森林重点调n_estimators、max_depth、min_samples_leaf;梯度提升(比如 LightGBM 或 XGBoost)重点调learning_rate、num_leaves、subsample。课程设计里不需要做完整的贝叶斯优化,用网格搜索跑一轮就够了。

from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV # 随机森林:先粗调,再细调 rf = RandomForestClassifier(random_state=42, n_jobs=-1) param_grid = { "n_estimators": [100, 200, 300], "max_depth": [5, 10, None], "min_samples_leaf": [1, 3, 5], "class_weight": ["balanced"] } grid = GridSearchCV( rf, param_grid, cv=5, # 5 折交叉验证 scoring="roc_auc", # 类别不平衡时 AUC 比准确率可靠 n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) print("最佳参数:", grid.best_params_) print("最佳 AUC:", grid.best_score_) best_rf = grid.best_estimator_ y_prob_rf = best_rf.predict_proba(X_test)[:, 1] print("测试集 AUC:", roc_auc_score(y_test, y_prob_rf))

scoring="roc_auc"是类别不平衡场景下的正确选择,用 accuracy 会选出偏向多数类的模型。cv=5在数据量少于 1000 条时够用,数据多可以降到 3 节省时间。n_jobs=-1用满所有 CPU 核心,但如果你在笔记本上跑,注意散热。min_samples_leaf设大一点(比如 3 或 5)能有效防止过拟合,代价是模型稍微保守一些。

3.3 特征重要性怎么看、怎么用

训练完模型不是终点,课程设计报告里需要解释“哪些因素驱动了返乡决策”。随机森林和梯度提升都提供特征重要性,但默认的基于不纯度的的重要性对高基数特征有偏。更可靠的做法是用排列重要性(permutation importance),它直接衡量“打乱某个特征后模型性能下降多少”。

from sklearn.inspection import permutation_importance # 排列重要性,在测试集上计算 perm_imp = permutation_importance( best_rf, X_test, y_test, n_repeats=10, # 重复 10 次取平均 random_state=42, scoring="roc_auc" ) imp_df = pd.DataFrame({ "feature": X_test.columns, "importance_mean": perm_imp.importances_mean, "importance_std": perm_imp.importances_std }).sort_values("importance_mean", ascending=False) print(imp_df.head(15))

n_repeats=10是精度和耗时的折中,课程设计够用。importance_std大的特征说明其重要性不稳定,解释时要谨慎。如果“外出务工年限”排第一且方向为负(年限越长越不愿返乡),这符合直觉——在外扎根越深,回流成本越高。如果某个特征重要性很高但方向反直觉,先别急着写进报告,回头查数据质量。

4. 避坑与排查:返乡预测项目里最容易翻车的五个地方

4.1 标签泄漏:准确率 99% 的模型为什么不能用

现象:模型在测试集上准确率 99%,AUC 接近 1.0,但换一批数据就掉到 60%。

原因:特征里混入了标签相关的信息。比如“是否已在本市缴纳社保”这个字段,如果数据是在返乡行为发生之后采集的,那它本质上就是标签的另一种表达。再比如做特征工程时用了全量数据的均值来填充缺失值,测试集的信息泄漏到了训练集。

解决:逐个检查特征,问自己“这个信息在做出返乡决策之前能拿到吗”。填充缺失值一律用训练集的统计量,不要用全量数据。如果某个特征重要性异常高(比如单独一个特征 AUC 就 0.95),大概率有问题。

4.2 类别不平衡:准确率 92% 但一个返乡人员都没预测出来

现象:测试集准确率 92%,但混淆矩阵显示正类召回率为 0。

原因:返乡意愿为“是”的样本只占 8%,模型发现全部预测为“否”就能拿到 92% 准确率,于是躺平了。

解决:训练时加class_weight="balanced",评估时看 AUC、F1 和召回率而不是准确率。如果正样本实在太少(少于 50 条),考虑 SMOTE 过采样,但要注意只在训练集上做,测试集保持原始分布。

4.3 分类型字段编码顺序搞反

现象:逻辑回归系数显示“受教育程度越高越不愿返乡”,但实际数据明明是相反的。

原因:LabelEncoder按字母顺序编码,如果“小学”被编成 5、“研究生”被编成 1,方向就反了。

解决:有序变量用手动映射(如 2.2 节的edu_map),不要用LabelEncoder。无序变量用独热编码。编码完成后打印一下映射关系,肉眼确认一遍。

4.4 训练集和测试集分布不一致

现象:交叉验证 AUC 0.85,测试集 AUC 0.65。

原因:随机划分时没做分层,或者数据本身有时间漂移。比如训练集里 2020 年数据多,测试集里 2023 年数据多,而疫情后返乡意愿整体上升了。

解决:分类任务一律加stratify=y。如果有年份字段,按时间切分而不是随机切分。切分后对比训练集和测试集的特征分布,差异大的特征要做分布对齐或直接剔除。

4.5 模型可解释性被忽略

现象:答辩时被问“为什么这个样本被预测为愿意返乡”,答不上来。

原因:只用了黑箱模型,没有做任何解释性分析。

解决:至少保留逻辑回归作为可解释基线。树模型用 SHAP 值做单样本解释,或者用排列重要性做全局解释。课程设计报告里放一张特征重要性排序图,比堆十个模型都有说服力。

5. 从课程设计到可复现项目:模型持久化与推理脚本的落地技巧

做完模型训练只是课程设计的一半,另一半是让别人能复现你的结果。我见过太多报告里只贴了训练代码,但没保存模型和特征处理流程,换台机器就跑不起来。正确的做法是把特征工程和模型一起打包,用joblib或pickle持久化,再写一个独立的推理脚本。

import joblib from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder # 把预处理和模型串成 Pipeline,避免推理时遗漏步骤 preprocessor = ColumnTransformer([ ("num", StandardScaler(), ["age", "work_years", "monthly_income"]), ("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), ["current_city", "industry"]) ]) full_pipeline = Pipeline([ ("prep", preprocessor), ("clf", RandomForestClassifier( n_estimators=200, max_depth=10, min_samples_leaf=3, class_weight="balanced", random_state=42)) ]) full_pipeline.fit(X_train, y_train) # 保存完整 pipeline,推理时直接加载 joblib.dump(full_pipeline, "return_migration_model.pkl") print("模型已保存") # 推理脚本:加载后直接预测新样本 loaded_model = joblib.load("return_migration_model.pkl") new_sample = pd.DataFrame([{ "age": 32, "work_years": 8, "monthly_income": 6500, "current_city": "深圳", "industry": "制造业" }]) prob = loaded_model.predict_proba(new_sample)[0, 1] print(f"返乡概率: {prob:.2%}")

用Pipeline的好处是所有预处理步骤跟着模型一起走,推理时不会出现“训练时标准化了、推理时忘了”的低级错误。handle_unknown="ignore"保证推理时遇到训练集里没出现过的城市名不会报错,而是编码为全零。保存的文件建议同时存一份特征列表和标签映射,方便别人理解。

还有一个容易被忽略的点:推理脚本要能处理缺失值。真实场景里新样本的某些字段可能为空,如果直接抛异常就很不友好。可以在 Pipeline 前面加一个SimpleImputer,用训练集中位数填充数值字段、用众数填充分类型字段。这样即使输入不完整,模型也能给出一个合理的概率估计。

最后说一个我自己的习惯:每次跑完训练,我会把X_train.columns、标签编码映射、模型超参数一起写进一个model_meta.json,和 pkl 文件放在同一个目录。半年后回头看这个项目,不用翻代码就知道当时用了哪些特征、标签 1 代表什么。这个习惯帮我省过很多次后悔药,希望也能帮到你。

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

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

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

立即咨询