数据分析项目报告:B站风格世界人口动态排序轮播图
1. 项目概述
1.1 项目背景
动态排序轮播图(Bar Chart Race)是数据可视化领域的经典形式,尤其在 B 站等视频平台广受欢迎。
本项目旨在通过 Python 数据分析与可视化技术,完整复刻高质量的世界人口年度动态排序轮播图,展
示 1960 年至 2024 年间世界各国人口总数的历史变迁。
1.2 项目目标
数据纯净:完整剔除大洲、收入组、地区汇总等非国家数据,仅保留 195 个真实主权国家。
动态展示:图表自动按年份轮播,每年严格按人口总数降序排列(人口越多的国家越靠图表上
方)。
全交互体验:支持悬浮显示具体人口数值、暂停/播放控制、手动切换年份。
商业级美化:采用深色背景、动态配色、圆角数值标签及丝滑轮播节奏,达到 B 站专业级可视化
标准。
2. 技术栈与运行环境
编程语言:Python 3.x
数据处理: pandas (用于数据读取、清洗、宽长表转换)
数据可视化: pyecharts (Bar 柱状图, Timeline 时间线轮播组件)
前端配置: JsCode (用于实现动态颜色映射), CDN 镜像配置 (解决国内网络加载空白问题)\
# 1. 导入核心工具importpandasaspd# 数据处理库frompyecharts.chartsimportBar,Timeline# 柱状图、时间线轮播组件frompyechartsimportoptionsasopts# 配置项工具frompyecharts.globalsimportThemeType,CurrentConfig# 内置主题、CDN配置frompyecharts.commons.utilsimportJsCode# 动态颜色JS工具# 修复CDN,解决HTML空白问题(国内网络必加)CurrentConfig.ONLINE_HOST="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/"3. 数据获取与探查 (阶段 1)
3.1 数据加载
项目读取本地 CSV 文件 世界人口数据-中文版(1960-2024).csv ,由于包含中文字符,指定编码为
gbk 。
# 2. 读取中文版人口数据集df=pd.read_csv('E:\jupeyter code\VS code\data\世界人口数据-中文版(1960-2024).csv',encoding='gbk')df.head()| Country Name | Country Code | Indicator Name | Indicator Code | 1960 | 1961 | 1962 | 1963 | 1964 | 1965 | ... | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 阿鲁巴 | ABW | 人口,总数 | SP.POP.TOTL | 54922.0 | 55578.0 | 56320.0 | 57002.0 | 57619.0 | 58190.0 | ... | 108727.0 | 108735.0 | 108908.0 | 109203.0 | 108587.0 | 107700.0 | 107310.0 | 107359.0 | 107995.0 | NaN |
| 1 | NaN | AFE | 人口,总数 | SP.POP.TOTL | 130075728.0 | 133534923.0 | 137171659.0 | 140945536.0 | 144904094.0 | 149033472.0 | ... | 623369401.0 | 640058741.0 | 657801085.0 | 675950189.0 | 694446100.0 | 713090928.0 | 731821393.0 | 750491370.0 | 769280888.0 | NaN |
| 2 | 阿富汗 | AFG | 人口,总数 | SP.POP.TOTL | 9035043.0 | 9214083.0 | 9404406.0 | 9604487.0 | 9814318.0 | 10036008.0 | ... | 34700612.0 | 35688935.0 | 36743039.0 | 37856121.0 | 39068979.0 | 40000412.0 | 40578842.0 | 41454761.0 | 42647492.0 | NaN |
| 3 | NaN | AFW | 人口,总数 | SP.POP.TOTL | 97630925.0 | 99706674.0 | 101854756.0 | 104089175.0 | 106388440.0 | 108772632.0 | ... | 429454743.0 | 440882906.0 | 452195915.0 | 463365429.0 | 474569351.0 | 485920997.0 | 497387180.0 | 509398589.0 | 521764076.0 | NaN |
| 4 | 安哥拉 | AGO | 人口,总数 | SP.POP.TOTL | 5231654.0 | 5301583.0 | 5354310.0 | 5408320.0 | 5464187.0 | 5521981.0 | ... | 29183070.0 | 30234839.0 | 31297155.0 | 32375632.0 | 33451132.0 | 34532429.0 | 35635029.0 | 36749906.0 | 37885849.0 | NaN |
5 rows × 70 columns
3.2 数据结构探查
通过 df.info() 探查,数据集基础结构如下:
样本量:266 行(包含国家及各类汇总区域)。
特征量:70 列。
属性列 (4列): Country Name (国家名称), Country Code (国家代码), Indicator Name
(指标名称), Indicator Code (指标代码)。
时间序列列 (66列): 1960 至 2025 年的数值列。其中 2025 年数据全为 NaN (缺失),有效数
据区间为 1960-2024 年。
df.info()<class 'pandas.core.frame.DataFrame'> RangeIndex: 266 entries, 0 to 265 Data columns (total 70 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Country Name 264 non-null object 1 Country Code 266 non-null object 2 Indicator Name 266 non-null object 3 Indicator Code 266 non-null object 4 1960 264 non-null float64 5 1961 264 non-null float64 6 1962 264 non-null float64 7 1963 264 non-null float64 8 1964 264 non-null float64 9 1965 264 non-null float64 10 1966 264 non-null float64 11 1967 264 non-null float64 12 1968 264 non-null float64 13 1969 264 non-null float64 14 1970 264 non-null float64 15 1971 264 non-null float64 16 1972 264 non-null float64 17 1973 264 non-null float64 18 1974 264 non-null float64 19 1975 264 non-null float64 20 1976 264 non-null float64 21 1977 264 non-null float64 22 1978 264 non-null float64 23 1979 264 non-null float64 24 1980 264 non-null float64 25 1981 264 non-null float64 26 1982 264 non-null float64 27 1983 264 non-null float64 28 1984 264 non-null float64 29 1985 264 non-null float64 30 1986 264 non-null float64 31 1987 264 non-null float64 32 1988 264 non-null float64 33 1989 264 non-null float64 34 1990 265 non-null float64 35 1991 265 non-null float64 36 1992 265 non-null float64 37 1993 265 non-null float64 38 1994 265 non-null float64 39 1995 265 non-null float64 40 1996 265 non-null float64 41 1997 265 non-null float64 42 1998 265 non-null float64 43 1999 265 non-null float64 44 2000 265 non-null float64 45 2001 265 non-null float64 46 2002 265 non-null float64 47 2003 265 non-null float64 48 2004 265 non-null float64 49 2005 265 non-null float64 50 2006 265 non-null float64 51 2007 265 non-null float64 52 2008 265 non-null float64 53 2009 265 non-null float64 54 2010 265 non-null float64 55 2011 265 non-null float64 56 2012 265 non-null float64 57 2013 265 non-null float64 58 2014 265 non-null float64 59 2015 265 non-null float64 60 2016 265 non-null float64 61 2017 265 non-null float64 62 2018 265 non-null float64 63 2019 265 non-null float64 64 2020 265 non-null float64 65 2021 265 non-null float64 66 2022 265 non-null float64 67 2023 265 non-null float64 68 2024 265 non-null float64 69 2025 0 non-null float64 dtypes: float64(66), object(4) memory usage: 145.6+ KB4. 数据清洗与预处理 (阶段 2)
原始数据为“宽格式”(一行一个国家,多年份平铺为多列),且包含大量非主权国家的汇总数据,无法
直接用于绘图。预处理分为以下核心步骤:
4.1 提取目标列
通过列表推导式筛选出所有列名为纯数字的列作为 year_cols ,保留 4 个属性列作为 attr_cols 。
year_cols=[iforiindf.columnsifi.isdigit()]#通过递推式筛选出所有列名为纯数字的列作为 year_cols#import numpy as np# year_cols=np.arange(1960,2026)attr_cols=['Country Name','Country Code','Indicator Name','Indicator Code']#保留 4 个属性列作为 attr_cols4.2 剔除无效与汇总数据 (核心清洗)
- 删除空值:直接删除 Country Name 为 NaN 的行(此类行通常为 AFE/AFW 等大洲代码汇总)。
df=df.dropna(subset=['Country Name'])- 黑名单过滤:定义包含 40+ 项的 black_list ,涵盖:
全球及大洲汇总(如“世界”、“东亚与太平洋地区”、“撒哈拉以南非洲地区”等)。
收入等级分类(如“高收入国家”、“中等收入国家”等)。
非主权地区/领地(如“阿鲁巴”、“中国香港特别行政区”、“波多黎各”等)。
使用 ~df[“Country Name”].isin(black_list) 进行反向过滤,最终精准保留 195 个 真
实主权国家。
# 2.黑名单:剔除大洲、收入等级、发展区域汇总black_list=[# 全球 & 大洲汇总"世界","北美","东亚与太平洋地区","欧洲与中亚地区","东亚与太平洋地区(不包括高收入)","欧洲与中亚地区(不包括高收入)","拉丁美洲与加勒比海地区","拉丁美洲与加勒比海地区(不包括高收入)","中东、北非、阿富汗与巴基斯坦","中东与北非地区(不包括高收入)","撒哈拉以南非洲地区","撒哈拉以南非洲地区(不包括高收入)","南亚","小国","加勒比小国","太平洋岛国","其他小国","未分类国家","阿拉伯联盟国家","欧洲联盟","欧洲货币联盟","经合组织成员","重债穷国 (HIPC)","脆弱和受衝突影響的情況下",# 收入等级"高收入国家","低收入国家","中等收入国家","中高等收入国家","中低等收入国家","中低收入国家",# 人口红利阶段"早人口紅利","後期人口紅利","預人口紅利","人口紅利之後",# 发展水平分类"最不发达国家:联合国分类",# 世界银行分组"IBRD与IDA","只有IBRD","只有IDA","IDA總","IDA混合","东亚与太平洋地区 (IBRD与IDA)","欧洲与中亚地区 (IBRD与IDA)","拉丁美洲与加勒比海地区 (IBRD与IDA)","中东与北非地区 (IBRD与IDA)","南亚 (IBRD与IDA)","撒哈拉以南非洲地区 (IBRD与IDA)",# 地区/领地/非主权地区"阿鲁巴","美属萨摩亚","百慕大","库拉索","开曼群岛","海峡群岛","法罗群岛","直布罗陀","格陵兰","关岛","中国香港特别行政区","中国澳门特别行政区","圣马丁(法属)","圣马丁(荷属)","北马里亚纳群岛","新喀里多尼亚","波多黎各","约旦河西岸和加沙","法属波利尼西亚","特克斯科斯群岛","英屬維爾京群島","美属维京群岛","科索沃"]df=df[~df["Country Name"].isin(black_list)].reset_index(drop=True)print("过滤后剩余国家数量:",df["Country Name"].nunique())过滤后剩余国家数量: 1954.3 宽表转长表 (Reshape)
使用 pd.melt() 函数将数据结构转换为适合时序分析的“长格式”:
id_vars : 保持不变的属性列。
value_vars : 需要展开的年份列。
生成新列: Year (年份) 和 Population (人口数值)
# melt:宽转长df_long=pd.melt(df,id_vars=attr_cols,# 不变字段:国家名称、编码等value_vars=year_cols,# 需要拆开的年份列var_name="Year",# 拆分成成新列的列名:年份value_name="Population"# 对应值的新列名:人口)df_long.head()| Country Name | Country Code | Indicator Name | Indicator Code | Year | Population | |
|---|---|---|---|---|---|---|
| 0 | 阿富汗 | AFG | 人口,总数 | SP.POP.TOTL | 1960 | 9035043.0 |
| 1 | 安哥拉 | AGO | 人口,总数 | SP.POP.TOTL | 1960 | 5231654.0 |
| 2 | 阿尔巴尼亚 | ALB | 人口,总数 | SP.POP.TOTL | 1960 | 1608800.0 |
| 3 | 安道尔共和国 | AND | 人口,总数 | SP.POP.TOTL | 1960 | 9510.0 |
| 4 | 阿拉伯联合酋长国 | ARE | 人口,总数 | SP.POP.TOTL | 1960 | 131334.0 |
4.4 数据类型转换与最终清理
将 Year 列转换为 int 类型。
将 Population 列转换为 numeric 类型,并使用 dropna 剔除转换后产生的空值(如 2025 年
的 NaN 数据)。
最终得到包含 12,675 条有效记录的干净数据集,仅保留 Country Name , Year , Population 三
列。
df_clean=df_long[["Country Name","Year","Population"]]# 仅保留国家名称、年份、人口列df_clean["Year"]=df_clean["Year"].astype(int)# 将年份列转换为整数类型df_clean["Population"]=pd.to_numeric(df_clean["Population"])# 将人口列转换为数值类型df_clean.dropna(inplace=True)# 剔除包含 NaN 的行df_clean.head()C:\Users\na\AppData\Local\Temp\ipykernel_23984\3147925960.py:2: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy df_clean["Year"]=df_clean["Year"].astype(int) # 将年份列转换为整数类型 C:\Users\na\AppData\Local\Temp\ipykernel_23984\3147925960.py:3: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame. Try using .loc[row_indexer,col_indexer] = value instead See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy df_clean["Population"]=pd.to_numeric(df_clean["Population"]) # 将人口列转换为数值类型 C:\Users\na\AppData\Local\Temp\ipykernel_23984\3147925960.py:4: SettingWithCopyWarning: A value is trying to be set on a copy of a slice from a DataFrame See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy df_clean.dropna(inplace=True) # 剔除包含 NaN 的行| Country Name | Year | Population | |
|---|---|---|---|
| 0 | 阿富汗 | 1960 | 9035043.0 |
| 1 | 安哥拉 | 1960 | 5231654.0 |
| 2 | 阿尔巴尼亚 | 1960 | 1608800.0 |
| 3 | 安道尔共和国 | 1960 | 9510.0 |
| 4 | 阿拉伯联合酋长国 | 1960 | 131334.0 |
5. 数据可视化与迭代优化 (阶段 4 - 6)
5.1 单年份横向柱状图测试 (阶段 4)
以 1990 年为例进行原型测试:
筛选 1990 年数据,按人口降序排列并取前 20 名。
使用 pyecharts.Bar 绘制,启用 reversal_axis() 将柱状图横向放置。
配置深色主题 ( ThemeType.DARK ),确保标签显示在柱子右侧 ( position=“right” ),验证基础渲
染逻辑无误。
# 数据准备test_df=df_clean[df_clean["Year"]==1990]test_df=test_df.sort_values(by="Population",ascending=False).head(20)#按人口排序,取前20名#绘制柱状图bar_test=(Bar(init_opts=opts.InitOpts(theme=ThemeType.DARK))#设置主题为深色.add_xaxis(test_df["Country Name"].tolist())#国家名字作为横轴.add_yaxis("人口",test_df["Population"].tolist())#人口作为纵轴.reversal_axis()#将柱状图横向放置.set_global_opts(title_opts=opts.TitleOpts(title="1990年世界人口排名",pos_left="center"),#设置标题legend_opts=opts.LegendOpts(is_show=False),#隐藏图例框).set_series_opts(label_opts=opts.LabelOpts(position="right"))#设置标签显示在柱子右侧)#bar_test.render("bar_test.html") #将图表渲染为html文件,在浏览器中查看bar_test.render_notebook()<div id="504c2a09894c44e99f38a924fa62473d" style="width:900px; height:500px;"></div>5.2 Timeline 轮播基础版 (阶段 5)
遍历清洗后的所有唯一 Year 。
每年动态生成一个 Bar 实例,并通过 Timeline.add() 方法将其按年份追加到时间轴中。
配置基础轮播参数: is_auto_play=True , play_interval=700 (毫秒), is_loop_play=True 。
导出为 人口轮播_中文_基础版.html ,验证时序动画连贯性。
year_list=df_clean["Year"].unique().tolist()#获得年份列表#初始化时间线timeline=Timeline(init_opts=opts.InitOpts(width="1500px",height="820px",theme=ThemeType.DARK,bg_color="#0a0a0a"))#遍历年份列表,每个年份画一个柱状图,添加到时间线中foryearinyear_list:# 数据准备data_df=df_clean[df_clean["Year"]==year]data_df=data_df.sort_values(by="Population",ascending=False).head(20)#按人口排序,取前20名data_df=data_df.sort_values(by="Population",ascending=True)#再次升序排序,保证大的在上country=data_df["Country Name"].tolist()#国家名字列表population=data_df["Population"].tolist()#人口列表#绘制柱状图bar=(Bar(init_opts=opts.InitOpts(theme=ThemeType.DARK))#设置主题为深色.add_xaxis(country)#国家名字作为横轴.add_yaxis("人口",population)#人口作为纵轴.reversal_axis()#将柱状图横向放置.set_global_opts(title_opts=opts.TitleOpts(title=f"{year}年世界人口排名",pos_left="center"),#设置标题legend_opts=opts.LegendOpts(is_show=False),#隐藏图例框).set_series_opts(label_opts=opts.LabelOpts(position="right"))#设置标签显示在柱子右侧)#将柱状图添加到时间线中timeline.add(bar,str(year))#配置基础轮播参数:自动播放、播放间隔600毫秒、循环播放timeline.add_schema(is_auto_play=True,play_interval=600,is_loop_play=True)timeline.render("基础轮播图.html")timeline.render_notebook()<div id="adc22ca8bf5b4439a9e433e9d8ff3b26" style="width:1500px; height:820px;"></div>