news 2026/9/10 12:49:15

Python+Pandas实战:全球生育率下降趋势分析与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Pandas实战:全球生育率下降趋势分析与可视化

生育率下降是被反复讨论的人口话题,马斯克关于“生育率崩溃比黑死病更致命”的说法更是把这一话题推到公共视野。先不评价这个历史比较是否准确,单从技术角度来看,这句话背后的核心变量是“总和生育率”。对数据分析师和技术开发人员来说,比起争论观点,更容易推进的方式是:把生育率数据下载下来,清洗成可用样本,按时序和区域做趋势分析,再用可视化表达结果。这篇文章会围绕这条主线,使用 Python、Pandas 和 Matplotlib 完成一个可复现的生育率数据分析小项目。

读者可以结合自己的实际需求,把本文的操作扩展成人口统计看板、报告自动刷新脚本,或者教学用的数据分析案例。学完后,你会得到一套从数据获取到图表展示的完整链路,同时也能理解总和生育率、更替水平、数据缺失和趋势解读这些关键概念。

1. 先理解“生育率崩溃”真正对应的指标是什么

1.1 总和生育率不是“某年生了多少孩子”

总和生育率(Total Fertility Rate,简称 TFR)指的是:如果一批女性按照某个年份的年龄别生育率走完整个生育周期,平均每位女性会生育多少个孩子。它是一个时期指标,反映的是“当前这一年各年龄段女性的生育强度”,并不是真实某一批人最终生育的子女数。

这个区别非常重要。某个年份的 TFR 特别低,不一定等于未来人口一定会快速减少。因为人口变化还受年龄结构、预期寿命、迁移和生育年龄推迟等因素影响。但 TFR 仍然是判断生育水平最常用、最适合跨地区比较的指标。

更替水平决定人口长期稳定所需的最低生育水平。在低死亡率环境下,普遍认为总和生育率大约需要达到 2.1,也就是平均每位女性生育 2.1 个孩子,才能保证下一代人口规模不出现长期自然减少。这个 2.1 并不是物理常数,而是由出生性别比和死亡率共同决定的。

1.2 数据能验证什么,不能验证什么

用公开数据可以验证下面几种问题:

  • 全球范围内低生育率地区是否在扩大。
  • 不同国家总和生育率在过去几十年里的下降速度。
  • 当前低于更替水平的国家数量和人口覆盖范围。
  • 区域之间的差异是否明显。

但数据不能直接验证“生育率崩溃比黑死病更致命”这种历史比较。因为黑死病时期的死亡数据、人口基数和统计口径与现代完全不同,直接比较会引入大量方法学问题。更合理的做法是先把“生育率是否在长期走低”这个可计算的部分分析清楚,再谈政策和社会影响。

1.3 本文分析主线

后面所有内容都围绕一条分析流程展开:

  1. 从开放数据源获取总和生育率时间序列。
  2. 将宽表结构整理成适合分析的长表结构。
  3. 检查缺失值、异常值和区域编码。
  4. 按年份计算低于更替水平的国家数量。
  5. 绘制时间趋势图和区域对比图。
  6. 对结果做谨慎解读,并说明数据边界。

这条流程可以复用到出生率、死亡率、预期寿命、GDP 等任何具有“国家-年份-指标”结构的公开数据上。

2. 准备分析环境:Python 数据分析的最小组合

2.1 环境要求

建议使用 Python 3.10 或更高版本。核心依赖只有三个:

依赖用途
pandas数据读取、清洗、透视和分组计算
requests请求开放数据接口,下载 JSON 或 CSV
matplotlib绘制趋势折线图和分布图

安装依赖:

pip install pandas requests matplotlib

如果网络环境访问外网受限,可以将数据文件下载后放到本地目录,把后面的代码改成读取本地 CSV 即可。

2.2 数据源选择

世界银行开放数据提供了大量人口指标,其中总和生育率对应的指标代码是SP.DYN.TFRT.IN。该指标返回各国家和地区的年度总和生育率,时间跨度通常从 1960 年开始,更新周期以世界银行数据库发布节奏为准。

除了世界银行,联合国《世界人口展望》也提供历史估计和预测数据,更适合做长期人口预测。对本文演示来说,世界银行接口足够,因为它返回结构简单,适合用 requests 直接处理。

字段含义:

字段含义
country_code国家或地区 ISO3 编码
country_name国家或地区名称
year年份
fertility_rate当年总和生育率

2.3 推荐项目结构

建议把脚本拆成模块,方便后续扩展:

fertility_analysis/ ├── data/ │ └── raw/ │ └── fertility_raw.csv ├── output/ │ └── fertility_trend.png ├── download_data.py ├── analyze_fertility.py └── requirements.txt

第一次运行下载脚本时,把原始数据保存到data/raw/,之后不要再重复请求接口。这既方便离线分析,也能避免频繁访问外部服务。

2.4 环境检查清单

开始写代码前,可以按下面的顺序检查一次环境:

  1. python --version能正常输出 Python 版本。
  2. pip --version能正常输出 pip 版本。
  3. 上面三个依赖都已安装。
  4. 当前目录有data/raw文件夹。
  5. 网络可以访问世界银行开放数据接口。

这项检查看起来基础,但很多人是在跑代码报ModuleNotFoundError之后才发现环境没有对齐。

3. 下载并加载生育率数据

3.1 使用 requests 拉取世界银行接口

世界银行接口支持 JSON 格式返回。下面的脚本会请求全量数据,并把核心字段整理成 DataFrame:

import requests import pandas as pd WORLD_BANK_URL = ( "https://api.worldbank.org/v2/country/all/indicator/SP.DYN.TFRT.IN" "?format=json&per_page=20000" ) def download_fertility_data(url=WORLD_BANK_URL): response = requests.get(url, timeout=30) response.raise_for_status() payload = response.json() records = [] # World Bank JSON 返回结构: [元信息, 数据列表] for item in payload[1]: records.append({ "country_code": item.get("countryiso3code"), "country_name": item.get("country", {}).get("value"), "year": item.get("date"), "fertility_rate": item.get("value"), }) df = pd.DataFrame(records) df["year"] = pd.to_numeric(df["year"], errors="coerce") df["fertility_rate"] = pd.to_numeric(df["fertility_rate"], errors="coerce") return df.dropna(subset=["year"]) if __name__ == "__main__": df = download_fertility_data() df.to_csv("data/raw/fertility_raw.csv", index=False, encoding="utf-8") print(df.sample(5))

这段代码做了三件事:

  • 请求全量数据,per_page=20000是为了避免分页截断。
  • 从嵌套 JSON 中提取国家、年份和数值。
  • 把年份和数值转成数值类型,方便后续排序和计算。

注意,fertility_rate为空是常见情况,不一定表示缺失,有些年份数据没有发布或估计值。清洗时不要把NaN当成 0。

3.2 如果接口不可用,使用本地 CSV 替代

如果暂时不能访问接口,也可以准备一份结构相同的最小 CSV:

country_code,country_name,year,fertility_rate KOR,South Korea,2020,0.84 JPN,Japan,2020,1.33 USA,United States,2020,1.64 SWE,Sweden,2020,1.66 NGA,Nigeria,2020,5.29

读取方式与上面保持一致:

df = pd.read_csv("data/raw/fertility_raw.csv")

上面数值只用于演示分析流程,不代表最新官方数据。实际分析时,要以你下载到的原始文件为准。

3.3 保存原始数据后立即检查行数

下载完成后,不要急着画图。先执行一次基础检查:

print("共 {} 行记录".format(len(df))) print("年份范围: {} - {}".format(df["year"].min(), df["year"].max())) print("国家数量: {}".format(df["country_code"].nunique()))

预期结果应是一个 1960 年到最近年份的完整时间序列。如果年份范围异常,比如只有最近两年,很可能是请求参数或接口返回结构出了问题。

4. 数据清洗与质量检查:先解决缺失值和区域编码

4.1 删除空值并确认时间列是数值类型

世界银行返回的数据中,某些国家在某些年份没有数值。清理时适合只保留有fertility_rate的行:

df = df.dropna(subset=["fertility_rate"]) df["year"] = df["year"].astype(int)

把年份转成整数后,排序和分组会更快。如果后续要画时间轴,直接使用整数年份最省事。

4.2 区分主权国家与区域汇总

世界银行数据里既包含“China”这样的国家,也包含“East Asia & Pacific”这样的区域汇总,还包含“World”“High income”这类经济分组。分析低生育率国家数量时,如果不过滤区域汇总,会把“World”这样的实体也当成一个国家,影响统计结果。

可以先用一个列表排除常见汇总编码:

aggregate_codes = { "ARB", "CEB", "CSS", "EAP", "EAS", "ECS", "EMU", "EUU", "FCS", "HIC", "HPC", "IBD", "IBT", "IDA", "IDB", "IDX", "LAC", "LCN", "LDC", "LIC", "LMC", "LMY", "LTE", "MEA", "MIC", "MNA", "NAC", "OED", "OSS", "PRE", "PSS", "SAS", "SSA", "SSF", "SST", "TEA", "TES", "TLA", "TMN", "TSA", "TSS", "UMC", "WLD", } df_country = df[~df["country_code"].isin(aggregate_codes)].copy()

这个列表并不一定完整,后续一定要用df_country["country_name"].unique()人工检查一遍,确认没有把“World”“Euro area”之类的汇总实体混进来。

4.3 保留一个同时包含区域汇总的版本

如果想在图中展示区域整体趋势,不要直接丢掉汇总编码。可以保留两个 DataFrame:

df_all = df.copy() df_country = df[~df["country_code"].isin(aggregate_codes)].copy()

df_all用于观察区域和全球整体,df_country用于统计国家数量。这样能避免“宏观趋势”和“国家分布”互相污染。

4.4 常见数据质量问题

问题现象可能原因检查方式处理建议
缺失年份很多部分国家数据发布滞后按国家查看年份范围不强行插值,图上标出缺失区间
某些年份数值异常小国家规模小或数据估计不足查看该国全时间序列结合人口权重观察,不单独判断
出现多个区域汇总接口返回包含经济分组输出编码去重保留国家样本时过滤汇总编码
年份显示为字符串JSON 中 date 是文本检查 dtype使用pd.to_numeric转换

这里的经验是:不要只关注“能不能出图”,要关注“每一个点的含义是否清楚”。人口数据是宏观数据,一个异常值可能不是误差,而是一个小国或特殊年份的真实表现。

5. 从时间维度计算生育率趋势

5.1 计算每一年低于更替水平的国家数量

先把总和生育率与更替水平 2.1 进行比较,生成一个布尔列:

replacement_level = 2.1 df_country["below_replacement"] = df_country["fertility_rate"] < replacement_level yearly = ( df_country[df_country["year"] >= 1980] .groupby("year") .agg( total_countries=("country_code", "nunique"), below_countries=("below_replacement", "sum"), ) .reset_index() ) yearly["below_ratio"] = yearly["below_countries"] / yearly["total_countries"] print(yearly.tail(10))

这里有一个细节:below_replacement是 bool 列,sum()会把True按 1 求和,所以below_countries表示当年低于更替水平的国家数量。分母使用nunique,避免同一个国家在同一年出现多行记录。

5.2 找出下降幅度最大的国家或地区

想要看哪些国家的生育率下降最明显,需要把最早年份和最近年份对齐:

first_year = df_country["year"].min() last_year = df_country["year"].max() df_first = df_country[df_country["year"] == first_year][["country_code", "fertility_rate"]] df_last = df_country[df_country["year"] == last_year][["country_code", "fertility_rate"]] merged = df_first.merge(df_last, on="country_code", suffixes=("_first", "_last")) merged["change"] = merged["fertility_rate_last"] - merged["fertility_rate_first"] print(merged.sort_values("change").head(10))

这里的change是负数时表示下降。结果里会出现变动很大的国家,原因可能包括统计口径变化、冲突导致的人口迁移、数据缺失等。所以看到“某个国家生育率下降 5 个点”时,要先检查该国在两个年份的样本量,不要直接写结论。

5.3 区分不同区域的平均水平

如果需要看区域趋势,可以基于区域汇总数据做平均:

region_cols = ["East Asia & Pacific", "Europe & Central Asia", "World"] region_df = df_all[df_all["country_name"].isin(region_cols)].copy() region_pivot = region_df.pivot_table( index="year", columns="country_name", values="fertility_rate" ) print(region_pivot.tail(5))

pivot_table会把“年份”放在行、把“区域名称”放在列。这样直接生成的数据结构非常适合下一步画图。

5.4 输出一份可审查的结果表

分析脚本最好输出一份 CSV,方便人工复核:

yearly.to_csv("output/yearly_below_replacement.csv", index=False, encoding="utf-8-sig")

使用utf-8-sig编码,可以让 Excel 直接打开时不出现中文乱码。这是数据导出时非常实用的小技巧。

6. 可视化:用三张图说明生育率下降是否普遍

6.1 第一张图:全球国家“低于更替水平”比例

下面的折线图展示 1980 年以来低于更替水平的国家占比变化:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows 常见中文字体 plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(yearly["year"], yearly["below_ratio"], linewidth=2) ax.axhline(0.5, linestyle="--", color="gray", linewidth=1) ax.set_title("Yearly Share of Countries with TFR below 2.1") ax.set_xlabel("Year") ax.set_ylabel("Share of Countries") ax.grid(True, linestyle=":", alpha=0.6) fig.tight_layout() fig.savefig("output/below_replacement_share.png", dpi=150) plt.show()

ax.axhline(0.5)的作用是画一条 50% 参考线。它可以快速看出“超过一半国家低于更替水平”的时间点,比单纯看数据表格更直观。

6.2 第二张图:典型国家的时间序列对比

选择若干有代表性的国家,画在同一条横轴上:

focus_countries = ["South Korea", "Japan", "United States", "Sweden", "Nigeria"] focus_df = df_country[df_country["country_name"].isin(focus_countries)] fig, ax = plt.subplots(figsize=(10, 5)) for country_name, group in focus_df.groupby("country_name"): ax.plot(group["year"], group["fertility_rate"], label=country_name, linewidth=2) ax.axhline(replacement_level, linestyle="--", color="gray", linewidth=1) ax.text(1960, replacement_level + 0.1, "replacement level 2.1") ax.set_title("Total Fertility Rate Trends for Selected Countries") ax.set_xlabel("Year") ax.set_ylabel("Total Fertility Rate") ax.legend() ax.grid(True, linestyle=":", alpha=0.6) fig.tight_layout() fig.savefig("output/selected_country_trends.png", dpi=150) plt.show()

从图中能直观看出:低生育率并不是最近一两年才出现的现象,而是长期趋势。同时也能看到各国下降速度不一样,不是所有国家都处于同一个阶段。

6.3 第三张图:最新年份的国家分布

要判断“现在有多少国家处于低生育率”,可以把最新年份的数据按数值区间分组:

latest = df_country[df_country["year"] == last_year].copy() bins = [0, 1.3, 2.1, 3.0, 10] labels = ["very low (<1.3)", "below replacement (1.3-2.1)", "near 2.1-3.0", "above 3.0"] latest["level_group"] = pd.cut(latest["fertility_rate"], bins=bins, labels=labels) level_counts = latest["level_group"].value_counts().sort_index() fig, ax = plt.subplots(figsize=(8, 5)) level_counts.plot(kind="bar", ax=ax, color=["#d62728", "#ff7f0e", "#2ca02c", "#1f77b4"]) ax.set_title("Distribution of Countries by TFR in Latest Year") ax.set_xlabel("Fertility Level Group") ax.set_ylabel("Number of Countries") ax.grid(True, axis="y", linestyle=":", alpha=0.6) fig.tight_layout() fig.savefig("output/fertility_level_distribution.png", dpi=150) plt.show()

pd.cut是最常用的连续变量分箱函数。这里的分组阈值是演示用的,实际分析中可以根据研究目的调整。例如把生育率低于 1.3 定义为“极低生育率”,这是人口学中经常提到的概念之一。

6.4 如何解读图形

如果低于更替水平的国家占比长期上升,说明“低生育率普遍化”确实是一个可观察的数据现象。但要注意,图形展示的是“国家数量占比”,不是“人口数量占比”。

一个小国占国家总数的一半,其人口可能只占全球人口很小一部分。因此,如果需要回答“全球多少人生活在低生育率地区”,要按人口加权计算,不能只看国家数量。这一步是很多分析出错的地方。

7. 常见坑与排查路径

7.1 接口返回结构变化或超时

现象:请求世界银行接口时超时,或返回的 JSON 不是预期的两层结构。

可能原因:

  • 网络不稳定。
  • 请求参数写错,比如per_page拼写错误。
  • 接口返回错误信息而不是数据。

检查方式:

print(response.status_code) print(response.text[:500])

解决方案:先打印原始响应,确认返回是否正常。如果网络受限,建议直接在浏览器中下载 CSV,改为本地读取。不要在脚本里反复重试接口,应设置超时和重试上限。

7.2 同一国家同一年出现多行

现象:groupby后的国家数量明显比实际国家数多。

可能原因:世界银行返回的数据中包含区域汇总,或者原始 CSV 中预留了多级表头。

检查方式:

duplicated = df_country.duplicated(subset=["country_code", "year"]).sum() print(duplicated)

解决方案:先去除汇总编码,再对重复行去重。

7.3 中文标签在图片上显示为方框

现象:图形标题中文变成方框或乱码。

可能原因:matplotlib 默认字体不含中文字符。

检查方式:

import matplotlib.font_manager as fm fonts = [f.name for f in fm.fontManager.ttflist if "Hei" in f.name] print(fonts[:10])

解决方案:

  • Windows 设置SimHei
  • macOS 设置Arial Unicode MSPingFang SC
  • Linux 安装中文字体后重新设置字体。

另一个更稳妥的方案是图表全部使用英文标题,避免字体问题。这样代码在不同系统上运行结果更一致。

7.4 数据到底应不应该插值

人口数据中会出现国家某年缺失的情况。不要把缺失值随便用前后平均值填上,因为生育率趋势可能是非线性的,插值会产生并不存在的“平滑数据”。如果一定要补,至少要标注“该值是插值得到的”,并在报告中说明插值方法。

7.5 排查路径速查表

问题现象优先检查下一步
数据集为空接口返回状态打印响应文本
年份不连续数据源覆盖范围查看原始文档
国家数量异常汇总编码混入打印编码去重列表
出图中文乱码中文字体未设置改用英文字体或安装字体
结论与国家报告不一致统计口径不同确认年份和区域口径

8. 从分析回到判断:数据边界与扩展方向

8.1 现有分析能支持什么结论

通过上面的流程,能支持这类结论:在某些年份区间内,全球低于更替水平的国家数量明显增加,不同区域之间的总和生育率差异也很大。这类结论是描述性的,不涉及因果关系。

不能直接得出的结论包括:

  • 生育率下降一定会导致人口崩溃。
  • 某一国家生育率低就一定会影响长期经济增长。
  • 历史时期的人口死亡事件与现代生育率可以直接比较。

因此,写分析报告时,措辞要落在具体的统计结果上,例如“在最新年份,本数据集中有多少国家低于 2.1”,而不是“生育率已经崩溃”。

8.2 生产环境中的数据任务还要补充什么

如果这是一次性的学习分析,上面脚本已经够用。但如果要长期监控生育率变化,还需要考虑:

  • 原始数据落盘:每次下载都保留带时间戳的原始文件。
  • 定时任务:使用 cron 或 Airflow 定期拉取。
  • 日志与告警:记录请求失败、数据缺失率和异常波动。
  • 版本管理:固定依赖版本,避免 pandas 或 matplotlib 升级后结果变化。
  • 权重字段:如果要看人口覆盖比例,需要导入各国人口数据。

下面是一个简单的调度示例(Linux cron):

0 2 1 * * cd /path/to/fertility_analysis && /usr/bin/python download_data.py

这样每月 1 日自动拉取一次数据,再执行分析。

8.3 下一步扩展方向

如果想把分析做得更深入,可以按下面的顺序扩展:

  1. 加入人口权重,计算“全球人口中生活在低生育率国家的比例”。
  2. 加入年龄结构数据,绘制人口金字塔。
  3. 使用联合国《世界人口展望》的预测数据,观察低生育率的长期影响。
  4. 对比出生率、死亡率、净迁移率,构建更完整的人口变化模型。
  5. 把分析结果接入 Web 框架,做成一个小型数据看板。

8.4 对新手最重要的练习建议

不要把目光只停留在画图上。真正值得反复练习的是:拿到一份陌生数据后,先描述它是什么结构,再清洗,再验证,最后才画图。每一步都要问“这个字段为什么会为空”“这个异常值说明什么”“这个分组是否合理”。

生育率数据只是其中一个合适的练习对象。用同样的流程去分析其他人口指标,可以更快掌握数据分析的基本功。回到开头那句“生育率崩溃比黑死病更致命”,与其急着站队,不如先把数据拿到手里,用统计结果去检验它背后的长期趋势。这种行为模式,才是做技术分析最有价值的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 12:48:10

MATLAB实战K均值聚类:从算法原理到调优技巧

1. 项目概述&#xff1a;从数据混沌到清晰分群当你手头有一堆数据点&#xff0c;它们看起来杂乱无章地散落在多维空间里&#xff0c;你想从中找出一些内在的规律&#xff0c;比如哪些客户属于同一类型&#xff0c;哪些基因表达模式相似&#xff0c;或者一张图片里哪些像素颜色接…

作者头像 李华
网站建设 2026/9/10 12:49:13

如何把PDF格式转换CAD图纸?三种实测有效的方法

PDF转CAD到底是在转什么&#xff1f; 简单来说&#xff0c;把PDF格式的文件转成DWG&#xff08;CAD图纸的通用格式&#xff09;&#xff0c;就是把原本已经“定型”的图纸内容&#xff0c;重新解析成CAD软件里可以编辑的矢量线条、文字和标注。需要注意的是&#xff0c;转换效…

作者头像 李华
网站建设 2026/9/2 0:39:04

微信小程序商城源码深度拆解:从项目结构到核心链路实战

简介&#xff1a;微信小程序作为轻量化应用形态&#xff0c;其开发模式融合了前端工程化与移动端特性&#xff0c;已成为电商业务的重要载体。理解小程序的项目结构、状态管理与组件化设计&#xff0c;是高效开发的基础。在实际工程中&#xff0c;登录态管理、SKU规格联动、购物…

作者头像 李华
网站建设 2026/9/2 21:36:09

射频无线充电的芯片级整合:Dialog与Energous合作解析

不用铺垫了&#xff0c;直接说。作为一个在半导体电源管理圈里混了不少年的人&#xff0c;看到Dialog Semiconductor和Energous官宣无线充电合作的消息&#xff0c;第一反应不是“又一家厂商抱团了”&#xff0c;而是“这个剧本我好像见过&#xff0c;但这次男主换了”。先说结…

作者头像 李华
网站建设 2026/9/2 4:00:57

TOPSIS多属性决策法:从原理到Python实战,量化选择最优方案

1. 项目概述&#xff1a;从“选哪个好”到“量化决策” 做项目、搞研究、甚至生活中选手机、挑学校&#xff0c;我们总会遇到一个经典难题&#xff1a;面对一堆各有优劣的选项&#xff0c;到底哪个才是“最好”的&#xff1f;比如&#xff0c;公司要采购一批服务器&#xff0c;…

作者头像 李华
网站建设 2026/9/1 22:57:32

AI工作流驱动Vibe Marketing:从感性氛围到可量化增长的工程化实践

1. 从“感觉”到“系统”&#xff1a;我理解的Vibe Marketing与AI工作流去年初&#xff0c;我还在为一个新消费品牌头疼&#xff1a;内容团队每天吭哧吭哧生产几十篇小红书笔记&#xff0c;数据却像过山车&#xff0c;爆款纯靠运气&#xff0c;转化路径更是模糊不清。直到我把“…

作者头像 李华