Leslie人口增长模型:矩阵构造、Python实现与避坑指南
简介资源包内是一份关于中国人口增长预测的完整建模文档doc格式主要由Logistic人口阻滞增长模型与按年龄结构的Leslie模型两套方案构成面向数学建模竞赛参赛者、人口统计学爱好者及需用MATLAB做预测分析的科研人员。文档基于1954—2005年多组历史数据建模通过对比不同数据起点给出了高精度的中期与长期人口预测并进一步分析了人口老龄化、人口抚养比等评价指标附有模型残差分析、优缺点评价与推广建议。该压缩包包含1个doc文件大小988KB内容结构完整涵盖问题重述、合理假设、符号说明、模型建立与求解、结果分析等模块便于系统研读。目前已有5153人学习下载是需要快速获取建模思路、核对计算细节或用于课程论文参考的实用资料。1. 先说结论Leslie人口增长模型到底帮你解决什么问题如果你做过一次区域人口预测多半会踩过同一个坑用总人口乘一个固定增长率第一年看着合理第三年就开始偏越往后越不敢看。原因是所有人被当成了一个「平均人」年龄结构被抹掉了。Leslie人口增长模型的做法完全不同——它把人口按年龄拆成一列有序的桶再用生育率和存活率两张表驱动整列数据向前滚动。这个模型能同时回答三个问题未来总量是多少、老龄化到什么程度、劳动年龄人口还有多少。适合的人群很明确做区域人口规划、建模竞赛、论文里的人口数据拟合以及想读懂人口预测结果背后逻辑的分析师。接下来我会用一套 5 年龄组的完整示例从矩阵构造讲到参数踩坑。2. 递推逻辑与矩阵构造Leslie 模型为什么比增长率外推靠谱2.1 年龄结构向量把人口拆成一列有先后顺序的桶Leslie 模型最核心的假设是一个人的未来状态只由他当前的年龄组决定。于是人口被表示成一个向量每一格是一个年龄组的人数顺序从年轻到年长n(t) [n₀(t), n₁(t), n₂(t), …, nₘ₋₁(t)]这里的 n₀ 是 0 到 4 岁组n₁ 是 5 到 9 岁组以此类推。向量有两个硬性要求相邻两组之间年龄连续、互不重叠并且所有组相加正好等于总人口。一秒就能看出这和外推法的本质区别。增长率外推是把所有人揉成一个点Leslie 模型则保留了「人会长大」这个事实。今年出生的婴儿要过 15 年才进入劳动年龄组再过几十年才进入老年组。出生率下降的影响不会立刻体现在总量上而是先体现在幼年组人数上再逐步传导到劳动力市场。用外推法永远看不见这种延迟效应而 Leslie 模型天然具备这种传导能力。我做这类预测时一般默认会用 5 岁组而不是单岁组。原因有三一是官方年龄构成数据大多直接按 5 岁组发布单岁组需要额外拆分二是 5 岁组的存活率数据更容易从生命表中获得三是矩阵规模从 100 缩小到 20 左右手算和调试都更轻松。除非对某一岁有特别的政策模拟需求否则不建议用单岁组自找麻烦。2.2 生育率与存活率如何拼出一个 Leslie 矩阵有了向量之后还要回答一个问题明年的年龄结构如何从今年的年龄结构算出来答案分成两部分。第一部分是新生儿。n₀(t1) 由当期所有处在生育年龄的人群贡献每个年龄组贡献的量等于该组人数乘上该年龄组的生育率。注意生育率在这里是一个数组不是单一数值。第二部分是存量人口的成长。今年在年龄组 i 的人明年如果还活着就移动到年龄组 i1。移动的人数等于今年该组人数乘上存活率 sᵢ然后这些存活者进入下一组。这样一来整个下一期向量就由两套参数唯一决定。把这两部分合并成矩阵运算就是标准的 Leslie 矩阵构造方法。设总共有 n 个年龄组矩阵 L 大小为 n×n第一行依次放每个年龄组的生育率次对角线即第 i1 行、第 i 列依次放存活率 s₀, s₁, …, sₙ₋₂矩阵其余位置全部为 0。迭代公式写成 n(t1) n(t) × L行向量乘矩阵我习惯这种写法。很多人第一次构造时会在语义上犯迷糊矩阵第 i 行第 j 列的元素代表「当期第 j 组的人对下一期第 i 组的贡献」。生育率是当期各年龄组对下一期第 1 组的贡献所以全部放在第一行存活率是当期第 i 组对下一期第 i1 组的贡献所以放在次对角线。搞清楚「行是明年、列是今年」矩阵就不会写错。2.3 手工算例5 个年龄组跑一年长什么样光看公式容易飘我建议你跟着这个 5 年龄组算例走一遍。初始人口向量取n(0) [100, 90, 80, 60, 40]生育率数组取b [0, 1.08, 1.42, 0.72, 0.13]存活率数组取s [0.87, 0.93, 0.91, 0.88]于是下一期第一组人数是各组生育贡献之和0×100 1.08×90 1.42×80 0.72×60 0.13×40算出来是 259.2。第 2 组人数是第 1 组存活下来的部分0.87×100 87。第 3 组是 0.93×90 83.7第 4 组是 0.91×80 72.8第 5 组是 0.88×60 52.8。结果如下年份0-4岁组5-9岁组10-14岁组15-19岁组20-24岁组合计第 0 年10090806040370第 1 年259.28783.772.852.8555.5最后一组的 40 人没有继续转移因为我们的模型里已经没有了下一个年龄组。实际建模中最后一个年龄组通常是 85 岁及以上这一组的人明年还活着但还属于本组所以它的存活率应该反映「留在本组的概率」不能简单设成 0。上面这个算例只是为了演示矩阵骨架真正落地时最后一组的处理要单独考虑。你可以看到一个非常真实的特征这一年总人口从 370 暴涨到 555.5主要原因是生育率数组高得离谱总和生育率约 3.35。但第二年这个暴涨就会放缓因为今年出生的 259.2 人要到 15 年后才进入生育年龄当期生育主力依然是存量人口。这种「出生脉冲随年龄组逐步传导」的效果正是 Leslie 模型相对外推法的核心价值它能把一次出生高峰变成未来持续二十年的结构波动。3. 从统计口径到可运行代码Leslie 模型的数据准备与 Python 实现3.1 先把口径对齐年龄分组、性别与起止时点拿到真实数据后第一件事不是写代码而是对口径。我见过太多人把年鉴数据直接塞进矩阵结果预测总量和官方公布对不上最后发现是年龄组边界差了一岁。第一个要统一的是年龄分组。年鉴里常见的分组是「0-4、5-9、10-14……」这种含上限的写法即 0-4 指 0 到 4 周岁共 5 岁但也有的数据源用「1-4、5-9」开头少了 0 岁组这一格合并时容易错位。我的习惯是先把所有年龄组统一成「满周岁」口径再按 5 岁组重新聚合。单岁组转 5 岁组时注意每一组的实际跨度到底是 1 岁还是 5 岁不要只看标签。第二个要统一的是性别。Leslie 矩阵最经典的形式是单性别模型通常只跟踪女性人口因为生育率定义在女性身上男性人口再由出生性别比和女性预测结果附带推算。如果直接把总人口放进矩阵生育率却用的是女性口径预测总量会明显偏大。所以初始向量应当明确写成「女性各年龄组人数」或「两性合计各年龄组人数」中的一种并选择配套的生育率口径。第三个要统一的是时点。人口数据有年初、年末、年中三种口径生育率和死亡率通常基于年中人口计算。如果你拿年初人口配上年中生育率分母分子不一致算出的绝对贡献会系统性偏大或偏小。我在处理时一般规定初始向量用当年年初人口生育率数据换算成人均后按当年水平直接应用并把输出统一解释为「预测某年年初人口」。3.2 生育率与存活率的折算细节生育率这块最容易踩的坑是「总和生育率」和「年龄别生育率」混着用。年鉴里常常只公布总和生育率TFR也就是各年龄别生育率之和。Leslie 矩阵需要的是每一个年龄组的生育率不能直接把 TFR 填进第一行那样等于把总和当成每个年龄组的贡献新生儿数会放大好几倍。常见的做法是如果手头只有 TFR就找一个标准的年龄别生育模式按比例缩放使各年龄组生育率之和等于给定的 TFR。这里有一个更细的口径问题Leslie 单性别模型要求第一行的生育率是「活产女婴数 / 女性人数」而不是「活产总数 / 女性人数」。如果拿到的年龄别生育率是包含男女婴的总活产口径还需要乘上女婴占比一般是 100 / (100 出生性别比)。出生性别比各地不完全一致近年多在 104 到 112 之间建议用本地近五年的均值而不是拍脑袋定一个固定值。存活率也不要用「1 - 死亡率」去凑。死亡率是期内死亡人数除以暴露人口年与年之间、年龄组之间口径变化很大。正规做法是从生命表中读取5 岁组存活率等于 x5 岁时的尚存人数除以 x 岁时的尚存人数。如果你拿到的只有单岁生命表转 5 岁组时可以用相邻年份的存活率连乘近似。这个细节决定了长期预测的质量存活率差 0.01迭代 30 年后就是不可忽视的偏差。3.3 最小可运行实现一次迭代、多步预测与输出数据准备完毕剩下的事就是把这个逻辑写成代码。下面是我常用的最小实现用 Python 的 numpy 和 pandas 完成30 行以内能跑通全流程。import numpy as np import pandas as pd def leslie_forecast(init_pop, fertility, survival, years): init_pop : np.array, 各年龄组初始人口顺序从年轻到年长 fertility: np.array, 各年龄组对下一期第 1 组的贡献女婴口径 survival : np.array, 存活率长度比 init_pop 少 1 years : int, 预测期数 n init_pop.shape[0] L np.zeros((n, n)) L[0, :] fertility # 第一行所有年龄组对新生儿的贡献 for i in range(n - 1): L[i 1, i] survival[i] # 次对角线当期 i 组存活到下一期 i1 组 pop init_pop.astype(float) records [] for t in range(years 1): total pop.sum() youth pop[:3].sum() / total # 0-14 岁占比 elder pop[-3:].sum() / total # 60 岁及以上占比按需求可调 records.append([t, total, youth, elder]) pop pop L # 行向量乘矩阵得到下一期 return pd.DataFrame(records, columns[year, total, youth_ratio, elder_ratio])这段代码的逻辑是先构建 Leslie 矩阵 L用第一行放生育率、次对角线放存活率然后把初始人口复制一份逐年左乘矩阵得到新向量。记录表里我同时输出总人口、幼年占比和老年占比方便直接观察结构变化。如果你关心的是 15-64 岁劳动年龄人口把 pop[-3:] 换成对应的切片范围即可。这里有两个参数细节要注意。第一fertility 数组的长度必须等于年龄组数因为每个年龄组都可能对新生儿有贡献只是 0 到 14 岁组的贡献通常为 0第二survival 数组长度比年龄组数少 1对应最后一组不向下转移。如果你把 surviaval 写错长度矩阵构造时索引会越界或者更隐蔽地某一行多了一个 0导致最后一组人口凭空消失。我在调试时习惯打印 L 矩阵本身肉眼扫一遍对角线和第一行能省下大量排查时间。跑完这段代码你会得到一个逐年记录表。接下来的问题更关键怎么读懂这张表以及怎么让模型回答业务问题而不是停在「能跑出曲线」。4. 让模型回答真问题总量拐点、老龄化与长期趋势校验4.1 读结果人口峰值、老龄化率和劳动年龄人口多步预测跑完后第一步看总量拐点。把 total 列从第 1 年到第 N 年扫一遍找到最大值对应的年份就是预测口径下的人口峰值年份。峰值年份之前人口增长之后进入负增长。要注意的是峰值年份对生育率参数极其敏感——生育率哪怕只差 0.1峰值可能提前或推迟五到十年。所以报告中千万不要把峰值年份写成「某年人口达到最大值」这种单点结论正确的写法是「在中等生育率情景下人口峰值预计出现在基准年后的第 X 年附近」。第二步看老龄化率曲线。我习惯同时观察老年占比和劳动年龄人口占比两条线。老年占比持续上升不稀奇真正值得关注的是劳动年龄人口的绝对数量拐点。因为抚养比的分母是劳动年龄人口分子是幼年加老年两条曲线一交叉就是抚养负担最重的时期。Leslie 模型能算出这个交叉点在哪一年这是外推法给不了的结构性信息。第三步不要只看最终年的数字要看逐年的变化速度。某一年老年占比突然跳升通常不是模型问题而是你刚好把某一波婴儿潮送进了老年组。这种跳跃是真实的结构效应不是错误。我在给报告写解读时一般会回到年龄结构向量里定位到底是哪一组在驱动变化比对着总量曲线空想有用得多。4.2 生育率微调对 30 年后的影响一个敏感度扫描模型搭好后第一个要动手验证的问题是结论对哪个参数最敏感我的标准做法是做一个关于生育率的敏感度扫描也就是把生育率数组整体乘以一组系数比如 0.9、1.0、1.1然后对比 30 年后的总量和老龄化率。base_fertility np.array([0, 0.03, 0.42, 0.78, 0.45, 0.12, 0]) base_survival np.array([0.997, 0.998, 0.997, 0.996, 0.994, 0.99, 0.98, 0.95]) init_pop np.array([950, 1050, 1200, 1100, 950, 800, 600, 400]) for factor in [0.8, 0.9, 1.0, 1.1, 1.2]: df leslie_forecast(init_pop, base_fertility * factor, base_survival, 30) end_row df.iloc[-1] print(ffactor{factor:.1f}, total_30{end_row.total:.0f}, elder_ratio{end_row.elder_ratio:.3f})这段代码把生育率从基准值的 80% 扫到 120%输出第 30 年的总量和老年占比。跑完后你往往会发现一个反直觉的规律生育率下降 20%30 年后总人口并没有下降 20%可能只下降 8% 到 10%因为现在的出生人口要 20 多年后才进入生育年龄生育率变化对总量的传导是缓慢且滞后的。反而是老年占比对存活率更敏感因为老年组基数大存活率微调 1% 会在 30 年内持续累积。这个扫描的真正价值不是给出一个精确数字而是让报告读者明白任何单点预测都建立在「参数不变」的强假设上一旦换成多情景对比结论的稳健性立刻清楚。我在实际项目里会把扫描结果画成三条曲线叠在一张图上所有讨论都基于曲线区间而不是孤立的数值点。4.3 用最大特征值验证长期趋势别只信曲线多步预测跑出来后还有一个数学工具可以交叉验证长期趋势就是 Leslie 矩阵的最大特征值。矩阵的特征值决定了递推的长期行为如果最大实特征值 λ 大于 1人口长期增长小于 1长期收缩等于 1人口趋于稳定。对应的特征向量给出稳定年龄结构也就是不管初始分布什么样长期跑下去年龄结构都会趋近这个形状。eigvals, eigvecs np.linalg.eig(L) lambda_max max(eigvals.real) stable_structure eigvecs[:, eigvals.real.argmax()].real stable_structure np.abs(stable_structure) / np.sum(np.abs(stable_structure)) print(flambda_max {lambda_max:.4f}) print(stable_age_structure:, stable_structure.round(4))我这里取实部最大特征值并把这个特征值对应的右特征向量归一化得到稳定年龄结构。和前面逐期迭代的结果对照如果预测 50 年后各年龄组占比已经逼近这个向量说明模型进入了稳定状态如果还差很远说明初始年龄结构的影响还没消退预测期需要拉长才能讨论长期趋势。有一个关键概念叫年龄结构动量和这个特征向量直接相关即使生育率恰好等于更替水平也就是 λ 等于 1由于初始年龄结构偏年轻人口还会惯性增长几十年。这个现象在 Leslie 模型里表现得非常明显你把生育率调到刚好让 λ1然后跑预测头 20 年总人口依然上升后面才回落。如果你在报告里只给出一条总量曲线不解释这是动量效应读者很容易误读成「模型预测人口一直增长」。我一般在输出里同时打印 λ 值和稳定年龄结构让读者一眼看到长期均衡状态。5. 避坑实录Leslie 模型最容易翻车的 5 个具体场景5.1 矩阵内部语义错误行列放反、存活率放错格现象模型跑出来第一年正常第二年总人口暴跌或某一组人数突然归零逻辑上完全说不通。原因最常见的两种错误——把存活率放在了主对角线上导致当期人群在同一组里「原地复活」或者把生育率放在了某一列而不是第一行导致新生儿被算到了其他年龄组的转移项里。第二个原因更隐蔽矩阵是方阵第一行和次对角线看起来很像一旦填错位置程序不会报错只有结果异常。解决先用单组测试法定位。写一段小代码把初始人口设为 100 人集中在第一组生育率清零跑一期正确结果应该是这 100 人乘以存活率后出现在第二组。如果第一组或第三组出现了不该有的人数矩阵语义一定有问题。我在每次新建矩阵后都会跑这个测试全程不超过一分钟但能省掉后面几小时的无效调试。5.2 数据口径错误性别没拆、用总生育率代替年龄别生育率现象预测的总人口比官方历史数据高出一大截或者明明生育率在降总量却异常坚挺。原因最常见的是把全体人口塞进矩阵但生育率用的却是女性口径导致男性也在「生育」还有一种是把年鉴里的总和生育率直接填进第一行每个年龄组都按 TFR 贡献新生儿总贡献就变成了 TFR 的几十倍。解决第一步确认初始向量和生育率口径一致——单性别模型就用女性人口两性模型就要把生育率换算成包含男女婴的总活产口径。第二步检查第一行之和把 fertility 数组求和如果得到 3 以上多半是直接把 TFR 填进去了正确值应该在 1 到 2 之间取决于单性别还是两性口径。我在数据准备阶段会专门打印这两个校验量比到预测结果出来后再回头找原因快得多。5.3 时间与边界错误初始结构失真、预测期太长现象前 3 年的预测就和实际年鉴对不上或者预测 50 年以上时曲线平滑得可疑一旦和实际情况对比就发现系统性偏离。原因初始向量直接用总量除以年龄组数等于把人口平均摊到每个桶里完全抹掉了真实的年龄结构。年轻型人口结构下这种摊平会低估新生儿数量因为生育主力人群被稀释了年老型结构下则相反。另一个问题是预测期太长时矩阵参数被当作常数使用而实际生育率和存活率会随社会变化刚性递推会把误差逐年放大。解决初始向量必须用真实年龄别数据哪怕是抽样数据也要先按人口普查结构校准不能图省事。预测期方面我的经验是 20 到 30 年是单参数模型能给出相对可信结论的上限超过这个区间与其追求精确的绝对数值不如把参数改成多情景讨论趋势区间。第 4 章的敏感度扫描就是为这种场景准备的。6. 让 Leslie 模型真正落地的一个习惯多情景对比而不是单点预测6.1 三套参数一次跑完把「如果」变成可比较的曲线我给业务方交付预测结果时最深刻的教训是单点预测不管做得多细致都会被问一句「如果生育率变了呢」。与其到时候返工不如一开始就输出多情景对比。我的标准做法是固定存活率和初始结构只让生育率按三套系数变化一次跑完并整理成表情景生育率系数第30年总量相对基准第30年老年占比低生育率0.85偏低约 8%偏高约 2 个百分点基准1.00基准值基准值高生育率1.15偏高约 6%偏低约 1.5 个百分点这张表本身就能回答大多数「如果」问题。差异通常不在总量而在结构上——生育率越低老年占比抬升越快抚养压力越大。我一般还会额外跑一个存活率微调情景因为当模型被用于养老或医疗规划时老年组的存活率假设往往比生育率更影响结论。scenarios { low: (base_fertility * 0.85, base_survival), base: (base_fertility, base_survival), high: (base_fertility * 1.15, base_survival), } for name, (fert, surv) in scenarios.items(): df leslie_forecast(init_pop, fert, surv, 30) peak_year df[total].idxmax() print(f{name}: peak_year_offset{peak_year}, ftotal_30{df[total].iloc[-1]:.0f}, felder_30{df[elder_ratio].iloc[-1]:.3f})这里的关键不是代码而是交付习惯永远给区间不给单点永远解释参数假设不让读者自己猜。我在一次人口结构分析项目中就是因为把单点预测写得太满被要求一遍遍重算。后来改成三情景一次交付反而再也没被追问过。这个习惯我现在一直保留。希望帮到你。本文还有配套的精品资源点击获取