☰
人口预测不只能画趋势线:回归分析与时间序列特征工程实战
2026/10/15 9:22:06 网站建设 项目流程

简介:一份基于回归分析的人口数量预测课程实验论文,适合统计学专业学生和回归分析课程学习者参考。资源以人口数量为因变量,分别研究其与粮食产量、GDP等经济指标的关系,系统涵盖一元线性回归、多元线性回归以及非线性模型三大模块。一元部分包含指标选择、样本确定、散点图绘制与回归诊断;多元部分涉及数据中心化标准化、逐步回归、多重共线性检测与主成分分析;非线性部分则补充了曲线拟合和Logistic模型,并对各类模型进行对比分析。包体为1个doc格式文档,共442KB,完整呈现从摘要、目录到结论的论文结构,可作为课程设计或毕业设计的写作范本。已有395人学习下载,内容结构清晰、便于按章节查阅,适合需要快速掌握回归分析建模流程与论文规范的读者。

1. 人口预测不能只画一条趋势线:回归分析的正确打开方式

某个街道办的朋友拿着一张二十年的户籍人口表找我,说想用回归分析预测明年的人口。他用 Excel 做了个线性拟合,R² 高达 0.98,结果次年实际数据一出,预测值偏了快两个百分点。问题不在回归本身,而在于他把「时间序列外推」误当成了「静态拟合」——回归分析做人口预测,本质是要把时间变量拆解成滞后特征、窗口统计和周期性分量,而不是简单地对年份画一条线。这篇文章就围绕这条路展开:什么样的数据能喂给回归、特征怎么构造、验证怎么切分才不会自欺欺人,以及那些让我翻过车的坑。

适用人群很明确:做市政规划、区域需求预估、教育医疗资源配置的从业者,以及正在做人口相关课程设计的同学。你不需要深度学习框架,一套 pandas 加 scikit-learn 就能跑通。

2. 数据准备:先把历史人口序列“洗”成回归能用的样子

2.1 为什么不能直接对年份做回归:残差里藏着的自相关

人口序列和普通回归样本最大的区别在于,它不是一个独立同分布的观测集。相邻年份的人口数天然高度相关,如果你直接把年份作为唯一自变量,模型学到的其实是「全局平均趋势」,而不是「最近状态下的变化规律」。用一个简单实验可以看得非常直观。

import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error # 构造一个带有周期性波动的模拟人口序列 rng = np.random.default_rng(42) years = np.arange(1960, 2024) base = 300 + (years - 1960) * 2.5 # 长期增长趋势 seasonal = 8 * np.sin(2 * np.pi * (years - 1960) / 11) # 周期性扰动 noise = rng.normal(0, 1.5, len(years)) pop = base + seasonal + noise df = pd.DataFrame({"year": years, "pop": pop}) # 只拿年份做训练 X = df[["year"]] y = df["pop"] model = LinearRegression().fit(X, y) y_pred = model.predict(X) mape = mean_absolute_error(y, y_pred) / y.mean() * 100 print(f"MAPE: {mape:.3f}%")

这段代码构造了一个带长期趋势和周期性波动的模拟数据,再拿年份做线性拟合。你会看到 MAPE 不大,而残差图里一定存在明显的自相关结构——连续的残差同正同负,说明模型漏掉了一个重要的解释变量:上一期的人口状态。

参数上,seasonal的周期 11 年不是拍脑袋,人口数据里常见的出生率波动周期往往和国家政策、经济周期挂钩,你可以在自己的数据上做自相关图(ACF)来确认周期长度。noise的标准差设成 1.5,是为了模拟统计采集误差,实际数据里这个噪声可能更大。

2.2 三种常见数据口径与清洗策略

人口数据来源不同,坑也不同。我处理过三类来源:统计年鉴的年末常住人口、公安系统的户籍人口、卫生健康系统的管理人口。同一座城市,这三套数字能差出好几个点,因为口径不同——常住人口包含流动人口,户籍人口只算本地户口,管理人口介于两者之间。

我的建议是:选一个口径作为主序列,其他口径只做交叉验证参考。不要在同一个模型里混用,否则某个年份突然从「常住口径」跳到「户籍口径」,模型会以为人口发生了突变,产生虚假的波动信号。

清洗时需要注意几个点:一是空值,人口序列很少有空值,但可能出现异常跳变,比如行政区划调整导致某年人口暴增或骤减,这类点应该标记并剔除,或者用前后三年均值平滑;二是量纲统一,如果年鉴某年数据从「万人」变成了「人」,不换算就直接进模型,系数会失控。一个实用习惯是把所有数据先画出来,人眼扫一遍再动手建模。

3. 特征工程:把时间序列改造成回归问题的关键一步

3.1 滞后特征:让模型「记得」上一年发生了什么

回归模型本身没有记忆,但人口变化是渐进的,今年的规模大概率取决于去年的规模。所以最直接的特征就是滞后项。假设df是按年排序的序列,构造滞后 1 年的特征只需要一行 shift。

feature_df = pd.DataFrame({ "pop_lag1": df["pop"].shift(1), # 上一年人口 "pop_lag2": df["pop"].shift(2), # 前两年人口 "pop_lag3": df["pop"].shift(3), # 前三年人口 "year": df["year"], "pop": df["pop"] }).dropna() train = feature_df[feature_df["year"] < 2015] test = feature_df[feature_df["year"] >= 2015] X_train = train[["pop_lag1", "pop_lag2", "pop_lag3"]] y_train = train["pop"] X_test = test[["pop_lag1", "pop_lag2", "pop_lag3"]] y_test = test["pop"]

shift(1)的含义是把整个序列向下移动一行,也就是让pop_lag1这一列的第 t 行对应原始数据的第 t-1 年。dropna 去掉开头三年的缺失行,因为前三行没有足够的滞后值可用。这里选择滞后 3 年而非 1 年,是因为人口变化有惯性,过去三年的趋势比单独一年的值更稳定。

你可能会问:滞后项之间会不会高度相关?会,而且这是个需要正面处理的问题。滞后 1 年、2 年、3 年的人口数相关性可能都在 0.99 以上,直接放进最小二乘回归里,系数会很不稳定。常见做法是换成岭回归,或者只保留滞后 1 年再加一个「三年平均增长率」特征。我在实际项目中更倾向于后者,因为可解释性更强。

3.2 窗口统计与周期性编码:把「趋势」和「季节」显式写进模型

滞后特征解决的是「记忆」问题,但人口数据里还有两类信息它表达不了:一段时间的平均水平和周期性波动。前者用滚动窗口均值或增长率来表达,后者用周期编码来表达。

feature_df["pop_roll3_mean"] = df["pop"].rolling(3).mean() feature_df["pop_growth_rate"] = df["pop"].pct_change() # 年增长率 feature_df["cycle_sin"] = np.sin(2 * np.pi * (df["year"] - 1960) / 11) feature_df["cycle_cos"] = np.cos(2 * np.pi * (df["year"] - 1960) / 11)

rolling(3).mean()是过去三年的人口均值,它比单一年份更能代表「当前水平」,相当于一个低通滤波。pct_change()计算的是环比增长率,模型可以通过它捕捉人口从增长转向下降的拐点。cycle_sin和cycle_cos是周期编码的标准做法,用正弦和余弦两个分量可以拟合任意相位的周期波动。

滚动窗口的窗口大小值得调。窗口太短,抖动大;窗口太长,反应太慢。我在一个区域人口项目中试过 3 年、5 年、7 年,发现 3 年窗口在短期预测上的表现最稳,因为人口变化不像气温那样有严格周期,过长窗口反而把最近的转折信息抹平了。

3.3 从单变量到多变量:有哪些外部特征值得加入

人口预测不能只看历史人口。出生率、死亡率、迁入迁出净流量是直接的驱动因素,如果这些数据能拿到,就应该进模型。我经历过一个项目,单靠人口历史序列的模型预测误差一直在 1.5% 左右,加入本地新开工住房面积和小学在校生数之后,误差降到了 0.8%。原因很朴素:住房供给变化领先于人口迁入,而小学在校生数是一个已经被验证过的间接人口指标。

外部特征与人口序列之间通常存在滞后关系。住房开工到实际入住大约需要 1 到 2 年,小学新生入学人数反映的是约 6 年前的出生情况。构造特征时要对输入变量做 shift,让它的时间对齐到待预测年份。加入外部特征之前务必检查它们和人口总量之间的相关性和共线性,方法会在避坑章节展开。

4. 模型训练与验证:不正确的切分方式会让误差评估失真

4.1 随机切分是时间序列的大忌:为什么时序数据不能用train_test_split

这是我在一个图像处理 Demo 之外做时序模型时踩过的第一个大坑。train_test_split默认随机打乱样本,但在人口预测里,打乱意味着用 2015 年的数据预测 1960 年,再用 1970 年的数据预测 2020 年——这在时间逻辑上是荒谬的,却会让模型在验证集上表现得很好,然后一上线就翻车。

正确的做法是严格按时间顺序切分:训练集全部在测试集之前。下面这段代码展示了前向链式验证的基本结构。

from sklearn.linear_model import Ridge from sklearn.metrics import mean_absolute_error def forward_chain_validate(feature_df, target_col, min_train_years=20): """前向链式验证:逐步扩展训练集,预测下一个年年份""" years = sorted(feature_df["year"].unique()) errors = [] for i in range(min_train_years, len(years) - 1): train_years = years[:i] test_year = years[i] train = feature_df[feature_df["year"].isin(train_years)] test = feature_df[feature_df["year"] == test_year] feature_cols = [c for c in feature_df.columns if c not in ["year", target_col]] model = Ridge(alpha=1.0) model.fit(train[feature_cols], train[target_col]) pred = model.predict(test[feature_cols]) actual = test[target_col].iloc[0] errors.append(abs(pred[0] - actual) / actual * 100) return np.mean(errors), np.std(errors) mean_mape, std_mape = forward_chain_validate(feature_df, "pop") print(f"前向链式验证 MAPE: {mean_mape:.3f}% ± {std_mape:.3f}%")

每轮迭代把训练集从最早的年份扩展到当前年份,在下一个年份上做预测,然后记录误差。由于训练集每次都包含真实的历史数据,验证结果会非常接近真实上线后的行为。min_train_years一般设为 15 到 20,保证模型在早期有足够样本学习趋势。

与随机切分相比,前向链式验证的误差通常更悲观,但它才是真实的。随机切分出来的 0.2% 误差是假象,前向验证给出的 1.2% 才是你敢写进报告里的数字。

4.2 岭回归为什么不比线性回归复杂多少,却稳得多

人口特征里大量特征是强相关的:滞后 1 到 3 年、滚动均值、增长率,它们之间存在多重共线性。普通最小二乘回归在共线性下系数方差巨大,稍微改动几个数据点,系数就面目全非。岭回归通过 L2 正则化惩罚系数的大小,让相关特征平分权重而非让某一个特征独大。

关键参数是alpha。alpha 太小等于没加正则化,太大则把所有系数压向零。我在实践中用RidgeCV做 10 折交叉验证自动选 alpha,比手动试参更省时间。

from sklearn.linear_model import RidgeCV alphas = np.logspace(-3, 3, 100) model = RidgeCV(alphas=alphas, cv=5) model.fit(X_train, y_train) print(f"最优 alpha: {model.alpha_:.4f}")

np.logspace(-3, 3, 100)生成从 0.001 到 1000 的等比序列,覆盖从近乎无正则化到强正则化的范围。cv=5是交叉验证折数,时序数据里这里存在轻微的信息泄漏——交叉验证会随机折分数据,但在特征层面如果样本是时间相关的,交叉验证选的 alpha 可能偏小。所以我一般用前向链式验证外的最后一整段数据来复核 alpha。

如果你追求极致的可解释性,可以考虑用 LASSO 回归(L1 正则化)。它会将部分特征系数压到零,起到特征选择的效果,代价是当特征间相关性极强时,它可能随机保留其中某一个特征,导致模型的可解释性反而变差。业务端需要向领导解释「为什么这个特征有用」时,岭回归的表现更稳定。

4.3 回归模型对比:线性回归、岭回归、多项式回归到底选哪个

人口预测场景下,一个不争的事实是:多数时候线性模型就够用。人口序列在短期内的变化接近线性,加上滞后特征之后,模型已经能捕捉到主要变化。多项式回归可以拟合弯曲的轨迹,但多项式项数的选择是个玄学问题——项数太少拟合不了转折,项数太多则在边界处剧烈外扩,导致远期预测出现荒谬值。

直接用年份的多项式做预测是最容易翻车的方式。我记得某一次尝试用三次多项式拟合人口轨迹,模型在训练集内完美贴合,可一旦外推到训练集范围之外,三条曲线立刻以物理上不可能的速度偏离。因为多项式的外推行为完全由最高次项主导,而人口数据的长期趋势很难用一个有限阶多项式来逼近,高阶多项式并非人口序列的合理外推机制。

相比之下,滞后特征加岭回归的组合在短中期预测上表现最稳定,而且模型结构简单,出问题容易排查。多项式回归只在一种情况下我会考虑使用——数据本身表现出清晰的非线性增长或衰减趋势,且预测范围不超出训练集末端太远,这样会让多项式外推的破坏性得到一定限制。

5. 避坑指南:人口预测回归模型中 4 个常见的翻车现场

5.1 数据泄漏:归一化在全体数据上拟合,等于提前看了答案

现象:在验证集上表现极好,MAPE 不到 0.3%,但部署到真实环境后误差暴涨到 2% 以上。

原因:数据预处理阶段对全量数据做了归一化或标准化,测试集的均值和方差信息进入了训练过程。如果测试集数据的分布稍有变化,训练时的归一化参数就不再适用。

解决:先切分数据,再在训练集上做归一化并保存参数,用同一组参数转换测试集。

from sklearn.preprocessing import StandardScaler # 错误做法:scaler.fit(全量数据) # 正确做法: scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) # 只能在训练集上fit X_test_scaled = scaler.transform(X_test) # 测试集只用transform

5.2 多重共线性:系数正负号乱跳,模型无法解释

现象:滞后 1 年系数是 1.2,滞后 2 年系数是 -0.3,两者在业务逻辑上都难以解释;改变训练集时间范围后,符号反转。

原因:滞后 1 年、2 年、3 年的人口数量高度正相关,最小二乘为了最小化误差,会在这些特征之间互相抵消,产生不稳定的系数估计。

解决:优先使用岭回归;或者检查方差膨胀因子,将 VIF 大于 10 的特征剔除或合并。检查代码如下。

from statsmodels.stats.outliers_influence import variance_inflation_factor feature_cols = ["pop_lag1", "pop_lag2", "pop_lag3"] X_sub = feature_df[feature_cols].dropna() vif_data = pd.DataFrame({ "feature": feature_cols, "VIF": [variance_inflation_factor(X_sub.values, i) for i in range(X_sub.shape[1])] }) print(vif_data)

VIF 大于 10 表示该特征能被其他特征线性解释的程度太高,剔除后模型依然能保持原有精度,但稳定性好很多。

5.3 统计口径调整:同一个预测对象,数据却不是一个体系

现象:某个年份人口数据出现断崖式下跌或猛增,模型视为真实波动,预测值随之剧烈跳动。

原因:统计口径发生变化,例如原来统计常住人口,后来改用户籍人口,或者行政区划调整划入划出。

解决:在数据准备阶段就要识别断点。我一般会单独列一个「是否口径变化」的哑变量放进模型,或者直接剔除异常年份,用前后数据进行插补。如果断点信息无法确认,就画序列图找人工标记。

5.4 过度自信的误差区间:回归只给点预测,不给概率

现象:报告中写「预测 2030 年人口为 125.3 万人」,领导问「误差多大」,你答不上来或只能勉强给个 MAPE。

原因:线性回归的预测是条件均值,不是分布。当数据存在结构性变化(比如生育政策调整)时,预测区间会严重低估不确定性。

解决:用分位数回归或给预测值附加一个经验误差区间。我习惯的做法是用前向链式验证中最后一折的误差绝对值作为基准区间宽度,再把区间写成上下 5% 的弹性范围写进报告,并明确标注「基于当前数据趋势,不包含政策突变因素」。

6. 进阶用法:前向验证与残差自相关检验让模型「自证清白」

6.1 用残差自相关检验模型是否漏掉了时间信息

一个好的时序回归模型,残差应该接近白噪声——也就是说残差之间不应该存在明显的相关性。如果残差在滞后 1 阶上仍然显著相关,说明模型遗漏了重要的时间结构,需要回头补充特征。

检验方法可以用 Durbin-Watson 统计量,或者在 pandas 里算残差的 lag1 自相关:

import statsmodels.api as sm model = Ridge(alpha=1.0).fit(X_train, y_train) residuals = y_train - model.predict(X_train) # 用 Ljung-Box 检验残差是否为白噪声 ljung_box = sm.stats.acorr_ljungbox(residuals, lags=[10], return_df=True) print(ljung_box) # 简单自相关:看残差和残差滞后1期的相关系数 resid_series = pd.Series(residuals) lag_corr = resid_series.autocorr(lag=1) print(f"Lag1 自相关: {lag_corr:.3f}")

Ljung-Box 检验的 p 值如果小于 0.05,说明残差不是白噪声,你的模型还有改进空间。人口数据因为本身具有很强的惯性,残差自相关经常会差一点达标,此时优先做的一件事就是增加滞后特征。

6.2 把模型做成「滚动重估」:每年更新而非一次定型

一个被很多人忽略的事实是:人口预测模型需要每年重新训练。我不建议训练一次然后用十年——人口结构会变,增长率会变,甚至连特征之间的关系都会变。我现在的习惯是把整套训练代码封装成一个脚本,每年拿到最新年度数据后重新跑一遍,用前向链式验证自动评估本轮误差,再把结果汇报上去。

这样做的另一个好处是能及时观察特征系数是否发生方向性变化。比如某一年开始,新开工住房面积对人口迁入的影响力下降,系数变小,我就会意识到城市可能进入了存量发展阶段,需要调整外部特征的组合。

6.3 从年尺度到月尺度的扩展:回归结构完全不一样

有些场景需要月度或季度预测。人口数据最粗颗粒度通常是年度,如果需要月度数据,只能用插值近似,插值带来的误差往往比模型误差还大。我踩过这个坑之后,现在的原则是:如果业务上只需要年度预测,就坚持用年度模型;月度预测需求的背后,通常真正想解决的是教育招生、医疗床位配置这类资源规划问题,它们实际上需要的是年龄别人口结构,而不是总人口数。

这时候回归分析的局限就显现了——你需要的是一个分年龄、分性别的队列模型,回归只能作为总量校准的工具。这个边界越早想清楚,越不会在项目方向上走偏。

写到这里,有一个我长期坚持的细节想分享给你:每次完成模型训练后,不要只保存最终参数,把当时的训练数据快照、特征列表和验证结果一并存档。半年后回看时,这些信息往往比模型本身更能说明问题。有一次某个城市的人口增长突然放缓,我翻出两年前的存档,发现当时模型里一个外部特征的系数在 5% 显著性水平上已经开始下降,只是当时忽略了它的信号。从那之后,每次的存档和滚动重估就变成了项目里的固定动作。

人口回归预测是一个门槛不高、但细节极其丰富的方向。把数据口径、切分方式和特征构造想清楚之后,模型精度反而不是最大的瓶颈。希望这一篇能帮你在自己的数据上少走几趟弯路。

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

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

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

立即咨询