ARTICLE DETAIL

资讯详情

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

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

Python+Pandas实战:全球生育率下降趋势分析与可视化 生育率下降是被反复讨论的人口话题马斯克关于“生育率崩溃比黑死病更致命”的说法更是把这一话题推到公共视野。先不评价这个历史比较是否准确单从技术角度来看这句话背后的核心变量是“总和生育率”。对数据分析师和技术开发人员来说比起争论观点更容易推进的方式是把生育率数据下载下来清洗成可用样本按时序和区域做趋势分析再用可视化表达结果。这篇文章会围绕这条主线使用 Python、Pandas 和 Matplotlib 完成一个可复现的生育率数据分析小项目。读者可以结合自己的实际需求把本文的操作扩展成人口统计看板、报告自动刷新脚本或者教学用的数据分析案例。学完后你会得到一套从数据获取到图表展示的完整链路同时也能理解总和生育率、更替水平、数据缺失和趋势解读这些关键概念。1. 先理解“生育率崩溃”真正对应的指标是什么1.1 总和生育率不是“某年生了多少孩子”总和生育率Total Fertility Rate简称 TFR指的是如果一批女性按照某个年份的年龄别生育率走完整个生育周期平均每位女性会生育多少个孩子。它是一个时期指标反映的是“当前这一年各年龄段女性的生育强度”并不是真实某一批人最终生育的子女数。这个区别非常重要。某个年份的 TFR 特别低不一定等于未来人口一定会快速减少。因为人口变化还受年龄结构、预期寿命、迁移和生育年龄推迟等因素影响。但 TFR 仍然是判断生育水平最常用、最适合跨地区比较的指标。更替水平决定人口长期稳定所需的最低生育水平。在低死亡率环境下普遍认为总和生育率大约需要达到 2.1也就是平均每位女性生育 2.1 个孩子才能保证下一代人口规模不出现长期自然减少。这个 2.1 并不是物理常数而是由出生性别比和死亡率共同决定的。1.2 数据能验证什么不能验证什么用公开数据可以验证下面几种问题全球范围内低生育率地区是否在扩大。不同国家总和生育率在过去几十年里的下降速度。当前低于更替水平的国家数量和人口覆盖范围。区域之间的差异是否明显。但数据不能直接验证“生育率崩溃比黑死病更致命”这种历史比较。因为黑死病时期的死亡数据、人口基数和统计口径与现代完全不同直接比较会引入大量方法学问题。更合理的做法是先把“生育率是否在长期走低”这个可计算的部分分析清楚再谈政策和社会影响。1.3 本文分析主线后面所有内容都围绕一条分析流程展开从开放数据源获取总和生育率时间序列。将宽表结构整理成适合分析的长表结构。检查缺失值、异常值和区域编码。按年份计算低于更替水平的国家数量。绘制时间趋势图和区域对比图。对结果做谨慎解读并说明数据边界。这条流程可以复用到出生率、死亡率、预期寿命、GDP 等任何具有“国家-年份-指标”结构的公开数据上。2. 准备分析环境Python 数据分析的最小组合2.1 环境要求建议使用 Python 3.10 或更高版本。核心依赖只有三个依赖用途pandas数据读取、清洗、透视和分组计算requests请求开放数据接口下载 JSON 或 CSVmatplotlib绘制趋势折线图和分布图安装依赖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 环境检查清单开始写代码前可以按下面的顺序检查一次环境python --version能正常输出 Python 版本。pip --version能正常输出 pip 版本。上面三个依赖都已安装。当前目录有data/raw文件夹。网络可以访问世界银行开放数据接口。这项检查看起来基础但很多人是在跑代码报ModuleNotFoundError之后才发现环境没有对齐。3. 下载并加载生育率数据3.1 使用 requests 拉取世界银行接口世界银行接口支持 JSON 格式返回。下面的脚本会请求全量数据并把核心字段整理成 DataFrameimport requests import pandas as pd WORLD_BANK_URL ( https://api.worldbank.org/v2/country/all/indicator/SP.DYN.TFRT.IN ?formatjsonper_page20000 ) def download_fertility_data(urlWORLD_BANK_URL): response requests.get(url, timeout30) 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], errorscoerce) df[fertility_rate] pd.to_numeric(df[fertility_rate], errorscoerce) return df.dropna(subset[year]) if __name__ __main__: df download_fertility_data() df.to_csv(data/raw/fertility_raw.csv, indexFalse, encodingutf-8) print(df.sample(5))这段代码做了三件事请求全量数据per_page20000是为了避免分页截断。从嵌套 JSON 中提取国家、年份和数值。把年份和数值转成数值类型方便后续排序和计算。注意fertility_rate为空是常见情况不一定表示缺失有些年份数据没有发布或估计值。清洗时不要把NaN当成 0。3.2 如果接口不可用使用本地 CSV 替代如果暂时不能访问接口也可以准备一份结构相同的最小 CSVcountry_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 保留一个同时包含区域汇总的版本如果想在图中展示区域整体趋势不要直接丢掉汇总编码。可以保留两个 DataFramedf_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, oncountry_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( indexyear, columnscountry_name, valuesfertility_rate ) print(region_pivot.tail(5))pivot_table会把“年份”放在行、把“区域名称”放在列。这样直接生成的数据结构非常适合下一步画图。5.4 输出一份可审查的结果表分析脚本最好输出一份 CSV方便人工复核yearly.to_csv(output/yearly_below_replacement.csv, indexFalse, encodingutf-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], linewidth2) ax.axhline(0.5, linestyle--, colorgray, linewidth1) 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:, alpha0.6) fig.tight_layout() fig.savefig(output/below_replacement_share.png, dpi150) 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], labelcountry_name, linewidth2) ax.axhline(replacement_level, linestyle--, colorgray, linewidth1) 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:, alpha0.6) fig.tight_layout() fig.savefig(output/selected_country_trends.png, dpi150) 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], binsbins, labelslabels) level_counts latest[level_group].value_counts().sort_index() fig, ax plt.subplots(figsize(8, 5)) level_counts.plot(kindbar, axax, 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, axisy, linestyle:, alpha0.6) fig.tight_layout() fig.savefig(output/fertility_level_distribution.png, dpi150) 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 MS或PingFang SC。Linux 安装中文字体后重新设置字体。另一个更稳妥的方案是图表全部使用英文标题避免字体问题。这样代码在不同系统上运行结果更一致。7.4 数据到底应不应该插值人口数据中会出现国家某年缺失的情况。不要把缺失值随便用前后平均值填上因为生育率趋势可能是非线性的插值会产生并不存在的“平滑数据”。如果一定要补至少要标注“该值是插值得到的”并在报告中说明插值方法。7.5 排查路径速查表问题现象优先检查下一步数据集为空接口返回状态打印响应文本年份不连续数据源覆盖范围查看原始文档国家数量异常汇总编码混入打印编码去重列表出图中文乱码中文字体未设置改用英文字体或安装字体结论与国家报告不一致统计口径不同确认年份和区域口径8. 从分析回到判断数据边界与扩展方向8.1 现有分析能支持什么结论通过上面的流程能支持这类结论在某些年份区间内全球低于更替水平的国家数量明显增加不同区域之间的总和生育率差异也很大。这类结论是描述性的不涉及因果关系。不能直接得出的结论包括生育率下降一定会导致人口崩溃。某一国家生育率低就一定会影响长期经济增长。历史时期的人口死亡事件与现代生育率可以直接比较。因此写分析报告时措辞要落在具体的统计结果上例如“在最新年份本数据集中有多少国家低于 2.1”而不是“生育率已经崩溃”。8.2 生产环境中的数据任务还要补充什么如果这是一次性的学习分析上面脚本已经够用。但如果要长期监控生育率变化还需要考虑原始数据落盘每次下载都保留带时间戳的原始文件。定时任务使用 cron 或 Airflow 定期拉取。日志与告警记录请求失败、数据缺失率和异常波动。版本管理固定依赖版本避免 pandas 或 matplotlib 升级后结果变化。权重字段如果要看人口覆盖比例需要导入各国人口数据。下面是一个简单的调度示例Linux cron0 2 1 * * cd /path/to/fertility_analysis /usr/bin/python download_data.py这样每月 1 日自动拉取一次数据再执行分析。8.3 下一步扩展方向如果想把分析做得更深入可以按下面的顺序扩展加入人口权重计算“全球人口中生活在低生育率国家的比例”。加入年龄结构数据绘制人口金字塔。使用联合国《世界人口展望》的预测数据观察低生育率的长期影响。对比出生率、死亡率、净迁移率构建更完整的人口变化模型。把分析结果接入 Web 框架做成一个小型数据看板。8.4 对新手最重要的练习建议不要把目光只停留在画图上。真正值得反复练习的是拿到一份陌生数据后先描述它是什么结构再清洗再验证最后才画图。每一步都要问“这个字段为什么会为空”“这个异常值说明什么”“这个分组是否合理”。生育率数据只是其中一个合适的练习对象。用同样的流程去分析其他人口指标可以更快掌握数据分析的基本功。回到开头那句“生育率崩溃比黑死病更致命”与其急着站队不如先把数据拿到手里用统计结果去检验它背后的长期趋势。这种行为模式才是做技术分析最有价值的地方。
返回列表