ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python融合成绩、录取率与经济指标:从清洗到预测

用Python融合成绩、录取率与经济指标:从清洗到预测 简介这是一份面向中高级Python学习者与数据分析人员的实操型作业文档围绕成绩、大学录取、居民收入与社会经济四类真实数据集展开涵盖数据读取与基本描述、缺失值识别与均值插补、连续变量统计描述、相关系数计算与散点图绘制并延伸到一元及多元线性回归的建模与结果解读还涉及虚拟变量生成、年龄平方项构造、多表字段合并等进阶操作。资源包共1个文件为docx格式的作业文档压缩后约17KB题目与数据要求逐一对应结构紧凑便于按任务推进目前已有84人学习下载。借助这份材料读者可完整走通数据清洗、异常值处理、可视化探索到回归建模与结论解释的流程在Pandas与Numpy的实际调用中巩固统计分析与机器学习基础适合作为课程练习、项目作业或独立刷题的参考范本。1. 为什么我把成绩、录取率和经济指标放进同一张表里单独看一份成绩单、一个录取率数字或者一条 GDP 曲线都只能得到“某年某地发生了什么”的结论回答不了“为什么会这样”和“明年大概会怎样”。把成绩、录取率和经济指标放进同一套 Python 分析流程里本质上是把三个不同颗粒度的数据源对齐到一个时间轴和地区轴上然后用统计描述找出各自分布用相关性分析看它们之间有没有联动最后用回归或时序模型把“过去的关系”外推成“未来的预测”。这个方案最适合两类人一类是高校招生或教务人员需要根据往年分数和报考热度估算下一年录取线另一类是教育咨询或区域经济研究人员想量化“人均可支配收入每涨 1%某批次录取率会怎么变”这类跨领域关系。接下来这套流程我拆成数据清洗、统计口径、特征对齐、模型选择四个环节每一步都有可以直接抄的代码和参数。2. 数据准备三张表怎么合并成一张分析宽表2.1 先想清楚三个数据集的公钥是什么合并数据之前最重要的事不是写pd.merge而是确定连接键。成绩表的最小粒度通常是一行一条考生记录包含地区、年份、总分、位次录取率表一行通常是一个地区一个批次一个年份的录取人数和报名人数经济指标表一行是地区加年份的 GDP、人均可支配收入、居民消费价格指数。三张表的公共字段只有“地区 年份”所以这是典型的复合主键合并。实际操作中我会先分别读入三份 CSV做三件事统一地区编码、统一年份格式、统一列名风格。import pandas as pd scores pd.read_csv(scores.csv) # 考生成绩明细 admit pd.read_csv(admission_rate.csv) # 录取率统计 economy pd.read_csv(economy.csv) # 地区经济指标 scores[year] scores[year].astype(int) admit[year] admit[year].astype(int) economy[year] economy[year].astype(int) scores[region] scores[region].str.strip() admit[region] admit[region].str.strip() economy[region] economy[region].str.strip() # 只保留三个数据集都存在的地区-年份组合避免合并后出现大量空值 valid_keys set(zip(scores[region], scores[year])) \ set(zip(admit[region], admit[year])) \ set(zip(economy[region], economy[year])) scores scores.set_index([region, year]).loc[ [k for k in valid_keys if k in set(zip(scores[region], scores[year]))]].reset_index()这段代码看起来啰嗦但每一步都对应一个常见坑。astype(int)是为了防止年份被读成浮点或者带小数点的字符串str.strip()解决的是“北京 ”和“北京”这种肉眼看不见的差异。最后一步用集合交集求公共键是因为三个表来源不同覆盖面很可能不一样——某地经济数据只更新到前年或者某年录取率表里缺了两个地区直接merge会产生大量 NaN后面做相关性分析时这些空值会悄悄把样本量吃掉。2.2 合并后必须做的四类数据质量检查常见的做法是合并完立刻执行四个检查重复键检查、空值率检查、极值检查和跨表逻辑检查。前三个用 pandas 自带方法就能完成第四个需要人工写规则。# 检查合并后是否有重复的 地区-年份 行 dup_mask df.duplicated(subset[region, year], keepFalse) print(重复行数:, dup_mask.sum()) # 空值概览看哪些列缺失严重 print(df.isnull().mean().sort_values(ascendingFalse)) # 极值检查超出 3 个 IQR 的样本单独拎出来看 def flag_outliers(s): q1, q3 s.quantile(0.25), s.quantile(0.75) iqr q3 - q1 return (s q1 - 3 * iqr) | (s q3 3 * iqr) outlier_cols [total_score, admit_rate, gdp_growth] for col in outlier_cols: if col in df.columns: print(col, 异常样本数:, flag_outliers(df[col]).sum())逻辑检查是重头戏。我一般会检查三条规则录取率必须在 0 到 1 之间经济指标的缺失年份不能出现在成绩表里同一地区同一年的平均分和录取率方向不能矛盾——比如平均分大幅上涨但录取率也在涨这在国内现行招生模式下不太可能同时出现除非招生计划扩大了需要回原始数据确认。这三条规则写成一个函数用断言抛异常比人眼筛查靠谱得多。2.3 构造分析用的派生特征原始列通常不能直接建模。成绩表里的总分是一个绝对数字但不同年份试卷难度不同不同地区考生群体不同直接用原始分跨地区比较没有意义。所以我会生成三个派生特征该考生在本地区本年份的百分位排名、该地区该年份的平均分、该地区该年份的分数标准差。录取率表里通常只有录取人数和报名人数录取率要自己算。经济指标表里一般直接用 GDP 增速和人均可支配收入增速这两个数字已经是相对量可以跨年份比较。# 计算百分位排名按 地区年份 分组 scores[pct_rank] scores.groupby([region, year])[total_score].rank(pctTrue) # 汇总地区-年份层级的成绩统计量 region_stats scores.groupby([region, year]).agg( avg_score(total_score, mean), std_score(total_score, std), high_score_ratio(total_score, lambda x: (x 600).mean()) ).reset_index() # 录取率计算报名人数做分母 admit[admit_rate] admit[admit_count] / admit[apply_count]这三组特征分别解决一个问题。百分位排名解决“跨年份可比性”因为每年的满分和难度在变但位次是稳定的。high_score_ratio比平均分更稳健它不受一个极端高分的影响能直观反映一个地区的高分考生密度。录取率放在地区-年份粒度上是因为录取是按省份分配的招生计划以省为粒度最合理。之后所有相关性分析和建模都基于这些派生特征原始分数只保留在明细表里供查询。3. 统计口径拆解从描述统计到三组联动关系3.1 分组描述统计先看整体分布再看分组差异拿到分析宽表之后第一步是输出一组描述统计量。我习惯按年份分组看核心指标的中位数和四分位距而不是直接看均值。原因很简单地区之间的经济规模和考生规模差异极大均值会被几个大省拉偏中位数更能代表“典型地区”的情况。成绩分布还要单独看偏度和峰度这两个指标直接决定后面能不能用普通线性回归。desc df.groupby(year)[[avg_score, admit_rate, gdp_growth]].describe(percentiles[.25, .75]) print(desc.T) # 偏度峰度检查线性回归对正态性敏感的其实是残差但特征偏度太大会影响拟合稳定性 print(偏度:, df[[avg_score, admit_rate, gdp_growth]].skew()) print(峰度:, df[[avg_score, admit_rate, gdp_growth]].kurt())描述统计的价值是帮你建立“什么是正常”的基线。比如某年某省的平均分从 480 涨到 520如果只看这一个数字会觉得异常但配合std_score从 95 降到 80就能意识到可能是试卷难度降低了、考生整体水平更集中了而不是单个群体突变。偏度和峰度输出之后如果偏度绝对值大于 1我会考虑对特征做对数变换或者 Box-Cox 变换。这一步不做后面的线性回归很容易出现系数不稳定、换一个训练集系数就大幅变动的情况。3.2 三组相关关系成绩与录取率、经济与录取率、经济与成绩这是整个统计分析里信息密度最高的部分。我在项目里通常会计算三组相关系数而不是只出一个总体相关矩阵。第一组是地区-年份粒度下平均分和录取率的相关系数直觉上是负相关——录取率越低竞争越激烈高分考生越集中第二组是人均可支配收入增速和录取率的相关系数经济越好的地区通常教育资源更丰富录取机会也更多第三组是 GDP 增速和平均分的相关系数这一组最弱因为它中间隔着教育投入、师资、家庭收入等多个传导环节。from scipy.stats import pearsonr, spearmanr pairs [(avg_score, admit_rate), (income_growth, admit_rate), (gdp_growth, avg_score)] for x, y in pairs: if x in df.columns and y in df.columns: r_p, p_p pearsonr(df[x], df[y]) r_s, p_s spearmanr(df[x], df[y]) print(f{x} vs {y}: Pearson{r_p:.3f} (p{p_p:.4f}), Spearman{r_s:.3f} (p{p_s:.4f}))Pearson 相关系数衡量线性关系Spearman 衡量单调关系。我两个都算是因为教育数据里很多关系不是线性的——比如经济指标对录取率的影响可能存在一个门槛效应人均收入从 2 万涨到 3 万时录取率提升明显从 4 万涨到 5 万时变化就很小了。如果两者差异很大比如 Pearson 接近 0 但 Spearman 显著那说明关系存在但不是直线后面建模时我会上树模型或者加二次项。真实项目里成绩和录取率的负相关通常是最稳的经济指标相关的系数会随着地区差异浮动这属于正常现象不必强行解释。3.3 相关性分析的三个边界条件相关分析有边界这在教育数据里必须说清楚。第一样本量小于 30 时相关系数的置信区间宽到没有实用价值所以统计结果要同时输出样本量不能只报一个 p 值。第二相关性受异常年份影响极大像某年因突发事件导致报考人数骤降整个相关系数都会被这一年拉偏。处理办法是算一个“去掉该年份后的相关系数”做敏感性对比。第三地区粒度的相关性不能直接下推到个人粒度这是生态学谬误的典型场景——某地区平均分和录取率强相关不意味着这个地区某个具体考生被录取的概率和分数之间存在同样的相关性。# 敏感性检查逐年删除后看相关系数波动范围 base_r, _ pearsonr(df[admit_rate], df[avg_score]) r_list [] for yr in df[year].unique(): sub df[df[year] ! yr] r, _ pearsonr(sub[admit_rate], sub[avg_score]) r_list.append(r - base_r) print(逐年删除后的最大偏移:, max(abs(x) for x in r_list))这个敏感性检查代码很短但能省下大量“结论被质疑”的麻烦。如果删除某一年后相关系数从 -0.6 跳到 -0.2说明这个相关性被单个年份主导写报告时就要明确标注这一年的特殊性。我在实际项目中做这种检查通常会把最大偏移超过 0.15 的年份单独圈出来详细看那一年发生了什么。相关性分析的目标不是为了得到一个漂亮的数字而是为了找到稳定的、可解释的关系只有这样的关系才值得进入预测模型。4. 预测建模用回归和时间序列分别回答两个问题4.1 问题拆解预测录取率用截面回归预测平均分用时序外推拿到上面的分析宽表之后要明确预测任务有两个数学模型不一样。第一个任务给定某地区下一年的经济增速预测值预测该地区的录取率这是一个典型的截面回归问题用历史年份的数据训练用最新经济指标做预测。第二个任务预测某地区下一年的平均分这本质上是一个面板时间序列问题每个地区是一条独立的时间序列可以用线性外推也可以用滞后特征做回归。我不建议一上来就用 LSTM 或者 Prophet教育数据通常只有 5 到 10 年的历史深度学习在这类小样本时序上基本必翻车线性模型加正则化在这个数据量下反而是最稳的做法。4.2 用线性回归预测录取率特征选择与交叉验证录取率预测的特征我固定用三组本地区往年录取率滞后一期、本地区经济增速、全国平均录取率作为宏观背景。本地区往年录取率是最强的预测因子因为录取率受招生计划影响最大而招生计划在短期内是连续的经济增速捕捉的是报考意愿的变化GDP 增速高的年份考生更愿意留在本省这会改变竞争格局全国平均录取率用于吸收政策层面的共同冲击。from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import cross_val_score, TimeSeriesSplit from sklearn.pipeline import Pipeline, make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.impute import SimpleImputer feats [admit_rate_lag1, gdp_growth, national_admit_rate] data_ml df.dropna(subsetfeats [admit_rate]).copy() y data_ml[admit_rate] X data_ml[feats] tscv TimeSeriesSplit(n_splits4) rf RandomForestRegressor(n_estimators400, max_depth5, random_state42) scores cross_val_score(rf, X, y, cvtscv, scoringr2) print(各折 R2:, scores.round(3), 平均:, scores.mean().round(3)) model rf.fit(X, y) print(特征重要性:, pd.Series(model.feature_importances_, indexfeats).sort_values(ascendingFalse).round(3))时序预测的交叉验证不能用普通的 KFold那一套是模型在“用未来预测过去”会严重高估表现。TimeSeriesSplit 保证训练集始终在验证集之前这个细节就是这个项目里最容易翻车的地方。随机森林在特征数只有三个的场景里极难过拟合max_depth5限制树的复杂度保证模型不会去记住单个年份的极端波动。特征重要性输出后如果admit_rate_lag1的贡献占比低于 60%说明录取率本身的惯性不强需要回去检查数据是不是存在口径变化比如某年招生计划突然扩招了 20%这种政策冲击会打断惯性。代码里用了SimpleImputer和StandardScaler。前者用均值填充特征里的空值后者把特征标准化到零均值单位方差。虽然树模型对尺度不敏感但管线里保留这两个组件是为了让同一个 pipeline 在后面替换成线性模型时也能直接跑不用改代码结构。4.3 用时间序列预测平均分先做平稳性检验再外推平均分预测我用另一个思路先对每个地区单独提取成绩序列做一阶差分看是否平稳然后对平稳序列用线性趋势外推。不要试图用一个全局模型拟合所有地区因为各省的考生规模、试卷难度完全不同全局模型会把省份间的差异当成噪声处理。from statsmodels.tsa.stattools import adfuller import numpy as np def forecast_avg_score(series, n_forecast1, alpha0.05): series series.sort_index() adf_stat, adf_p, _, _, _, _ adfuller(series.dropna()) # 非平稳就先差分再对差分序列做线性外推 if adf_p alpha: diff series.diff().dropna() x np.arange(len(diff)) slope, intercept np.polyfit(x, diff.values, 1) last_diff slope * (len(diff) n_forecast - 1) intercept return series.iloc[-1] last_diff, adf_p else: x np.arange(len(series)) slope, intercept np.polyfit(x, series.values, 1) return slope * (len(series) n_forecast - 1) intercept, adf_p forecast_value, adf_p forecast_avg_score(df[df[region] 示例省].set_index(year)[avg_score]) print(f下一年预测平均分: {forecast_value:.1f}, ADF p-value: {adf_p:.4f})这段代码的核心逻辑是ADF 检验看序列是否平稳如果不平稳说明存在趋势或季节成分先差分把趋势去掉对差分序列做线性拟合再把差分预测值累加到最后一个观测值上。这个过程看起来简单但在只有 5 年数据的情况下比硬套 ARIMA 更稳定因为 ARIMA 的参数估计在短序列上方差太大。np.polyfit用的是最小二乘拟合如果差分序列还有明显的周期性——通常年度数据不会有但如果有就应该改用带季节分量的模型。输出里ADF p-value要不要写进报告我一般会写因为它是“这条预测线可信度”的直接证据。5. 避坑手册三份数据合流最容易踩的五个坑5.1 坑一地区口径对不齐“省”和“省本级”不是同一个东西现象是合并后某个地区的样本量骤减或者指标异常比如“广东”和“广东省”在两张表里同时存在但实际上指的是不同的统计范围。原因是数据来源不同一张表用的是行政区划代码另一张用的是自定义简称。解决方法是合并前先做地区字典映射把“广东”“广东省”“440000”统一成一个标准编码。要提醒的是不要用模糊匹配str.contains(广东)会把“广东”误配到“广东县”这种不存在的地区结果比不匹配更糟。5.2 坑二年份字段被 Excel 自动转换变成带小数点的日期现象是读出来的 year 列全是“2023.0”或者“2023-01-01”导致astype(int)直接报错。原因是原始 CSV 用 Excel 编辑过日期格式被自动套用。解决方法是读取时指定dtype{year: int}或者在read_csv之后统一用pd.to_datetime(...).dt.year来提取。这个问题在你跟教务或财务部门要数据时尤其常见他们保存 CSV 之前通常会开着 Excel 看一眼顺手就把格式改了。# 从常见日期格式里安全提取年份 def safe_extract_year(s): s s.astype(str).str[:4] return pd.to_numeric(s, errorscoerce).fillna(0).astype(int)这个函数处理的是“2023-01-05”“2023/1/5”“2023.0”三种格式的混合体统一取前四位转数字。errorscoerce保证无法解析的值变成 NaN 而不是抛异常。5.3 坑三录取率直接用录取人数除以报名人数忘记了“缺考人数”现象是某几个地区录取率大于 1检查数据发现分母小于分子。原因是报名人数是“报了名”的缺考者也在里面录取人数是“实际录取”的两者相除理论上不可能大于 1但某些小规模地区可能出现报名人数统计口径不含补录、而录取人数含补录的情况。解决办法是统一分母口径如果录取人数包含补录批次分母也要用“报名人数 补录报名人数”或者在分析时单独标记补录占比超过 5% 的样本不放进预测模型训练集。5.4 坑四用普通线性回归做录取率预测预测出负值现象是模型输出某个地区下一年录取率为 -0.03明显不合常理。原因是录取率是 0 到 1 之间的有界变量普通线性回归没有边界约束当特征组合极端时预测值就会越界。解决方法是换成逻辑回归或者对目标变量做 logit 变换把预测值压回 0 到 1 区间。logit 变换的问题是录取率等于 0 或 1 时无定义实际数据里很少出现这种情况但保险起见可以先做极值压缩把 0 替换成 0.001把 1 替换成 0.999。from sklearn.linear_model import LogisticRegression eps 1e-3 y_logit np.log((y.clip(eps, 1 - eps)) / (1 - y.clip(eps, 1 - eps))) model_lr LinearRegression().fit(X, y_logit) pred_logit model_lr.predict(X) pred_rate 1 / (1 np.exp(-pred_logit))这段代码先对目标变量做 logit 变换拟合线性模型之后再用 sigmoid 函数还原。模型系数的含义也变了X 每增加一个单位录取率的 log-odds 增加对应系数那么多解释起来要费点口舌但至少预测值落在合理区间里。5.5 坑五经济指标里“增速”和“增长率”混用结果相关性全部失真现象是经济指标列名写的是“GDP 增速”但数值看起来像是“GDP 总量”的百倍查原始文档发现实际是“GDP 增长率%”单位没有换算。解决办法是在读入数据后立刻统一量纲如果是百分数就除以 100 转成小数或者全部用原始值配合StandardScaler标准化。我比较推荐后者因为标准化之后线性模型的系数可以直接比较相对重要性而且不会因为某个特征量纲太大导致梯度下降不收敛。6. 进阶验证用残差图和 SHAP 确认模型不是在“自说自话”预测模型最怕的不是准确率低而是输出看起来很合理、实际上在自我重复。验证方法我固定用两个第一个是残差图看模型在不同录取率区间是否存在系统性偏差第二个是 SHAP 值看每个样本的预测值到底是由哪个特征驱动的避免模型只靠“上一年的录取率”这一个特征做惯性外推经济指标完全没起作用。import shap residuals y - model.predict(X) bins pd.qcut(y, 4, labelsFalse) group_resid pd.DataFrame({bin: bins, resid: residuals}).groupby(bin)[resid].mean() print(分段残差均值:, group_resid.round(4)) explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X) shap.summary_plot(shap_values, X, feature_namesfeats)分段残差均值如果呈现明显的单调趋势比如低录取率区间残差全是正的、高录取率区间残差全是负的说明模型在两端都有系统性低估或高估这时需要检查是不是缺少了某个特征比如招生计划的政策变量。SHAP 图看的是单个特征的贡献分布如果gdp_growth的 SHAP 值散点图呈现一条水平带说明经济指标对预测结果几乎没有实际影响特征重要性分数高可能只是偶然波动。遇到这种情况我会把经济指标从模型里去掉再训练一遍对比两者的验证集误差如果误差几乎不变那这个指标在当前数据量下就不值得写进报告里。写这份笔记的时候我一直在想其实三张表的合并只是开头真正花时间的是把每个数字的统计口径搞清楚。你拿到的数据越接近一手来源分析结果就越经得起推敲。如果你跑完这段流程发现自己的地区数量和年份跨度比示例小模型结果方差会大一些这很正常优先保证数据质量模型复杂度往后放。希望这些踩坑记录能帮你在自己的数据上少走几步弯路。本文还有配套的精品资源点击获取
返回列表