Python 百年奥运数据分析实战|Pandas 清洗 + Matplotlib/Pyecharts 可视化 + 拖拽大屏完整项目(附源码)
一、前言
大家好,今天分享一套大一课内数据分析完整实战项目 ——1896-2016 百年奥运数据探索与体育强国分析,基于 Kaggle 经典奥运数据集,完整覆盖数据读取、多表关联、缺失值清洗、特征工程、多维探索分析、中国专题、交互式可视化大屏全流程,适合数据分析入门练手、课程大作业、期末报告。
项目信息
- 数据集:Kaggle 120 年奥运历史数据(athlete_events.csv + noc_regions.csv)
- 数据规模:27 万 + 运动员参赛记录
- 技术栈:Pandas、NumPy、Matplotlib、Pyecharts
- 项目周期:课内 4h + 课外自主实践
- 学习目标:掌握多表合并、缺失值处理、BMI 特征衍生、静态 + 交互式可视化、拖拽式数据大屏开发
二、数据集介绍
1. 两张核心 CSV
-
athlete_events.csv 运动员明细表 | 字段 | 含义 | | ---- | ---- | | ID | 运动员唯一编号 | | Name、Sex、Age、Height、Weight | 姓名、性别、年龄、身高 (cm)、体重 (kg) | | NOC | 国家奥委会三位代码 | | Games、Year、Season、City | 赛事全称、年份、夏 / 冬奥、举办城市 | | Sport、Event | 大项、细分小项 | | Medal | 奖牌:Gold/Silver/Bronze/ 空(无奖牌) |
-
noc_regions.csv 国家代码映射表
- NOC:奥委会编码
- region:国家 / 地区全称
- notes:备注(历史政权、特殊代表团说明)
2. 数据痛点
- 大量身高、体重、年龄缺失;
- Medal 字段空值代表未获奖,不能直接删除;
- 历史政权代码冗余:URS (苏联)、EUN (独联体)、RUS (俄罗斯);GER/FRG/GDR (两德);
- 特殊 NOC:ROT 难民代表团、UNK 未知地区、TUV 小众岛国,无法匹配国家名称。
三、完整项目代码分步实现
步骤 1:环境导入与全局配置
python
运行
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
# 全局设置
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei'] # 中文显示
plt.rcParams['axes.unicode_minus'] = False # 负号正常显示
from pyecharts import options as opts
from pyecharts.charts import Bar, Line, Pie, Radar, Boxplot, Page
步骤 2:数据加载 + 左连接多表融合(阶段一)
使用左连接 Left Join保证所有运动员记录不丢失,关联国家名称
python
运行
# 读取数据
df_athletes = pd.read_csv("athlete_events.csv")
df_regions = pd.read_csv("noc_regions.csv")
# 左连接合并两张表
df = pd.merge(df_athletes, df_regions, on="NOC", how="left")
# 查看未匹配到国家的特殊NOC(难民、未知地区)
missing_NOC = df[df["region"].isna()]["NOC"].unique()
print("无匹配国家的NOC代码:", missing_NOC)
# 输出:['SGP', 'ROT', 'UNK', 'TUV']
# 数据基础统计
print(df.describe())
步骤 3:数据清洗 + 特征工程(阶段二核心)
3.1 缺失值填充策略
- region 空值填充为 Unknown(难民 / 未知代表团)
- Medal 空值统一标记
No Medal,区分获奖 / 未获奖 - 身高、体重、年龄缺失:身高用中位数(不受极端身高干扰),年龄、体重用均值
python
运行
# 1. 国家缺失填充
df["region"] = df["region"].fillna("Unknown")
# 2. 奖牌缺失填充
df["Medal"] = df["Medal"].fillna("No Medal")
# 3. 身体指标缺失填充
df["Height"] = df["Height"].fillna(df["Height"].median())
df["Weight"] = df["Weight"].fillna(df["Weight"].mean())
df["Age"] = df["Age"].fillna(df["Age"].mean())
# 衍生特征:BMI指数 = 体重(kg) / 身高(m)²
df["BMI"] = df["Weight"] / ((df["Height"] / 100) ** 2)
df["BMI"] = df["BMI"].round(2)
# 查看历史政权统一映射示例
print("俄罗斯相关NOC:", df[df["region"]=="Russia"]["NOC"].unique())
# ['RUS' 'URS' 'EUN'] 苏联、独联体、俄罗斯统一归类Russia
print("德国相关NOC:", df[df["region"]=="Germany"]["NOC"].unique())
# ['GER' 'FRG' 'GDR' 'SAA'] 东德、西德、统一德国合并统计
策略思考题解答
- 统一合并 URS/EUN/RUS 统计总奖牌优势:无需手动拼接多段政权数据,直接按 region 分组即可得到俄罗斯全历史奖牌;
- 单独分析俄罗斯联邦:增加过滤条件
df[(df["region"]=="Russia") & (df["Year"] >= 1992)],苏联解体后年份单独提取。
步骤 4:全球宏观探索分析(阶段三)
4.1 运动员基础画像可视化
python
运行
# 1. 男女参赛比例饼图
plt.figure(figsize=(8,6))
df["Sex"].value_counts().plot.pie(autopct="%1.1f%%", explode=(0.1,0), shadow=True)
plt.title("百年奥运男女运动员参赛占比")
plt.show()
# 2. 男女年龄箱线图
df.boxplot(column="Age", by="Sex", figsize=(8,5))
plt.title("男女运动员年龄分布")
plt.suptitle("")
plt.show()
# 3. 男女BMI分布直方图
plt.figure(figsize=(14,6))
plt.hist(df[df["Sex"]=="M"]["BMI"], bins=30, alpha=0.5, label="男性")
plt.hist(df[df["Sex"]=="F"]["BMI"], bins=30, alpha=0.5, label="女性")
plt.xlabel("BMI指数")
plt.legend()
plt.title("男女运动员BMI分布对比")
plt.show()
# 4. 历年男女平均年龄折线图
plt.figure(figsize=(14,6))
df[df["Sex"]=="M"].groupby("Year")["Age"].mean().plot(marker="o", label="男")
df[df["Sex"]=="F"].groupby("Year")["Age"].mean().plot(marker="*", label="女")
plt.title("1896-2016男女运动员平均年龄变化")
plt.xlabel("年份")
plt.ylabel("平均年龄")
plt.legend()
plt.show()
4.2 全球奖牌格局对比
python
运行
# 筛选所有获奖记录
df_medal = df[df["Medal"] != "No Medal"]
# 1. 全历史总奖牌Top20
top20_all = df_medal.groupby("region")["Medal"].count().sort_values(ascending=False).head(20)
top20_all.plot(kind="barh", figsize=(12,7), color="#0099cc")
plt.title("奥运百年总奖牌榜TOP20国家")
plt.xlabel("奖牌总数")
plt.show()
# 2. 1994苏联解体后现代格局奖牌Top20
df_modern = df_medal[df_medal["Year"] >= 1994]
top20_modern = df_modern.groupby("region")["Medal"].count().sort_values(ascending=False).head(20)
top20_modern.plot(kind="barh", figsize=(12,7), color="#ff6666")
plt.title("1994年后现代奥运奖牌榜TOP20")
plt.xlabel("奖牌总数")
plt.show()
# 3. 仅获得≤3枚奖牌的长尾小国
less_medal = df_medal.groupby("region")["Medal"].count()
less_medal = less_medal[less_medal <= 3]
less_medal.plot(kind="pie", figsize=(9,9))
plt.title("仅获得少量奖牌的国家分布")
plt.ylabel("")
plt.show()
步骤 5:中国奥运崛起专题分析(阶段四)
python
运行
# 提取中国全部数据
df_cn = df[df["region"] == "China"]
df_cn_medal = df_cn[df_cn["Medal"] != "No Medal"]
# 1. 夏/冬奥奖牌历年走势
summer_cn = df_cn_medal[df_cn_medal["Season"]=="Summer"].groupby("Year")["Medal"].count()
winter_cn = df_cn_medal[df_cn_medal["Season"]=="Winter"].groupby("Year")["Medal"].count()
plt.figure(figsize=(12,6))
summer_cn.plot(marker="o", label="夏季奥运")
winter_cn.plot(marker="*", label="冬季奥运")
plt.title("中国历届夏/冬奥会奖牌走势")
plt.legend()
plt.show()
# 2. 中国首金查询
first_gold_year = df_cn_medal[df_cn_medal["Medal"]=="Gold"]["Year"].min()
first_gold = df_cn_medal[(df_cn_medal["Year"]==first_gold_year) & (df_cn_medal["Medal"]=="Gold")]
print("中国首金年份:", first_gold_year)
print(first_gold[["Name","Sport","Event"]].head(1))
# 3. 男女奖牌历年堆叠柱状图
gender_year = df_cn_medal.groupby(["Year","Sex"])["Medal"].count().unstack(fill_value=0)
gender_year.plot(kind="bar", stacked=True, figsize=(13,6))
plt.title("中国历年男女运动员奖牌贡献")
plt.xlabel("年份")
plt.ylabel("奖牌数量")
plt.show()
# 4. 中国优势项目饼图
top_sport_cn = df_cn_medal["Sport"].value_counts().head(10)
top_sport_cn.plot(kind="pie", autopct="%.2f%%", figsize=(10,10))
plt.title("中国获奖最多十大运动项目")
plt.ylabel("")
plt.show()
步骤 6:Pyecharts 拖拽式可视化大屏(阶段六完整代码)
支持暗黑主题、自由拖拽调整布局,最终固化为正式大屏 HTML
python
运行
# 统一全局暗黑主题配置
TEXT_COLOR = "#ffffff"
BG_COLOR = "#0a0e27"
def get_base_opts(title_name):
return opts.InitOpts(bg_color=BG_COLOR, theme="dark", width="500px", height="350px")
# 图表1:历史总奖牌TOP10横向柱状图
def bar_medal_rank():
top10 = df_medals["region"].value_counts().head(10).sort_values()
c = (
Bar(init_opts=get_base_opts("历史总奖牌榜TOP10"))
.add_xaxis(top10.index.tolist())
.add_yaxis("奖牌总数", top10.values.tolist(), color="#00d2ff")
.reversal_axis()
.set_global_opts(
title_opts=opts.TitleOpts(title="百年奥运总奖牌榜TOP10", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)),
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(color=TEXT_COLOR)),
yaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(color=TEXT_COLOR))
)
)
return c
# 图表2:中美俄德英五国奖牌历年折线
def line_super_power():
top5 = ["USA", "China", "Russia", "UK", "Germany"]
years = sorted(df_medals["Year"].unique())
c = Line(init_opts=get_base_opts("五大体育强国历年奖牌走势"))
color_list = ["#ff4500", "#ffd700", "#00ff7f", "#1e90ff", "#da70d6"]
for country, color in zip(top5, color_list):
cnt = df_medals[df_medals["region"]==country].groupby("Year")["Medal"].count()
y_data = [cnt.get(y,0) for y in years]
c.add_yaxis(country, y_data, is_smooth=True, linestyle_opts=opts.LineStyleOpts(color=color))
c.add_xaxis([str(i) for i in years])
c.set_global_opts(
title_opts=opts.TitleOpts(title="五大强国历届奖牌趋势", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)),
xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(color=TEXT_COLOR)),
yaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(color=TEXT_COLOR))
)
return c
# 图表3:中国金牌优势项目雷达图
def radar_cn_gold():
cn_gold = df_medals[(df_medals["region"]=="China") & (df_medals["Medal"]=="Gold")]
top6_sport = cn_gold["Sport"].value_counts().head(6)
schema = [opts.RadarIndicatorItem(name=i, max_=int(top6_sport.max()*1.2)) for i in top6_sport.index]
c = (
Radar(init_opts=get_base_opts("中国金牌优势领域"))
.add_schema(schema=schema, splitarea_opts=opts.SplitAreaOpts(is_show=True))
.add("金牌数", [top6_sport.values.tolist()], color="#ffd700")
.set_global_opts(title_opts=opts.TitleOpts(title="中国金牌优势项目雷达图", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)))
)
return c
# 图表4:百年女性参赛比例面积图
def area_female_ratio():
gender_cnt = df.groupby(["Year","Sex"])["ID"].count().unstack(fill_value=0)
gender_cnt["Female_Ratio"] = (gender_cnt["F"] / (gender_cnt["M"] + gender_cnt["F"]) * 100).round(2)
c = (
Line(init_opts=get_base_opts("女性运动员参赛占比演变"))
.add_xaxis([str(i) for i in gender_cnt.index])
.add_yaxis("女性占比(%)", gender_cnt["Female_Ratio"].tolist(), areastyle_opts=opts.AreaStyleOpts(opacity=0.5, color="#ff69b4"), color="#ff69b4")
.set_global_opts(title_opts=opts.TitleOpts(title="百年奥运女性参赛比例变化", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)))
)
return c
# 图表5:热门参赛项目TOP10柱状图
def bar_sport_top10():
sport_cnt = df["Sport"].value_counts().head(10).sort_values()
c = (
Bar(init_opts=get_base_opts("参赛人次最多十大项目"))
.add_xaxis(sport_cnt.index.tolist())
.add_yaxis("参赛人次", sport_cnt.values.tolist(), color="#7b68ee")
.reversal_axis()
.set_global_opts(title_opts=opts.TitleOpts(title="热门运动项目TOP10", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)))
)
return c
# 图表6:篮球/举重/体操BMI箱线图
def box_bmi_compare():
sport_list = ["Basketball", "Weightlifting", "Gymnastics"]
df_bmi = df[df["Sport"].isin(sport_list)][["Sport","BMI"]]
df_bmi = df_bmi[(df_bmi["BMI"]>10) & (df_bmi["BMI"]<60)]
data_list = [df_bmi[df_bmi["Sport"]==s]["BMI"].tolist() for s in sport_list]
c = (
Boxplot(init_opts=get_base_opts("三大项目运动员BMI分布"))
.add_xaxis(sport_list)
.add_yaxis("BMI指数", Boxplot.prepare_data(data_list))
.set_global_opts(title_opts=opts.TitleOpts(title="不同项目运动员体格对比", textstyle_opts=opts.TextStyleOpts(color=TEXT_COLOR)))
)
return c
# 组装可拖拽大屏
page = Page(layout=Page.DraggablePageLayout)
page.add(
bar_medal_rank(),
line_super_power(),
radar_cn_gold(),
area_female_ratio(),
bar_sport_top10(),
box_bmi_compare()
)
# 生成草稿页面,自由拖拽布局
page.render("奥运大屏草稿.html")
print("草稿大屏已生成:奥运大屏草稿.html,打开拖拽调整布局")
# 布局固化代码(拖拽完成后执行)
# from pyecharts.charts import Page
# Page.save_resize_html(
# source="奥运大屏草稿.html",
# cfg_file="chart_config.json",
# dest="奥运最终可视化大屏.html"
# )
# print("固化大屏完成!")
大屏使用步骤
- 运行代码生成
奥运大屏草稿.html,Chrome 浏览器打开; - 拖拽、缩放所有图表,自定义大屏布局;
- 点击页面左上角
Save Config,自动下载chart_config.json; - 执行固化代码,生成无多余边框、固定布局的正式大屏 HTML。
四、核心分析结论(写报告直接复制)
1. 全球运动员画像
- 百年奥运男性参赛占比 72.5%,女性仅 27.5%,但女性参赛比例持续逐年上升,体现全球性别平等进程;
- 男性 BMI 整体高于女性,篮球、举重选手 BMI 显著高于体操运动员;
- 运动员平均年龄稳定在 24-26 岁区间,无明显高龄化 / 低龄化趋势。
2. 世界体育版图地缘变化
- 全历史榜单:美国、俄罗斯(含苏联、独联体)、德国长期稳居奖牌前三;
- 1994 年后现代格局:苏联解体后俄罗斯奖牌总量大幅下滑,中国稳步上升,挤进世界第一梯队;
- 长尾效应明显:超半数国家仅获得 1-3 枚奖牌,多为小型岛国、发展中国家。
3. 中国奥运崛起核心发现
- 爆发拐点:1984 年洛杉矶奥运会实现金牌零突破,2008 北京东道主奖牌达到峰值;
- 性别结构:早期女子奖牌占比极高(跳水、乒乓球、举重),近年男子项目成绩稳步提升,男女发展趋于均衡;
- 优势项目:跳水、乒乓球、体操、举重、射击是中国夺金基本盘,符合 “二八定律”,80% 金牌来自 20% 优势项目;
- 冬季奥运起步晚,但短道速滑逐步成为冬奥核心夺金项目。
4. 社会学验证结论
- 东道主效应:主办国当年奖牌数显著高于前后两届,主场优势客观存在;
- 女性平权:1896 首届奥运无女性参赛,2016 里约女性参赛占比突破 40%,是全球女性权益发展的缩影;
- 政权更迭直接改变奖牌榜单格局,奥运数据是地缘政治、国家综合实力的直观镜像。
五、项目拓展思考题(课程作业加分项)
- 为什么身体指标缺失值身高用中位数、体重 / 年龄用均值?
- 统一 URS/EUN/RUS 为 Russia 分组统计有什么优缺点?单独分析俄罗斯联邦如何过滤年份?
- 数据集查询首金与历史许海峰是否一致?数据偏差来源是什么?
- 如何设计帕累托图验证 “80% 奖牌来自 20% 优势项目”?
- 如何通过 City、Year 字段自动识别每届东道主,量化验证东道主光环?
六、项目总结
本项目完整复刻企业级数据分析流水线:数据加载→多表关联→清洗补全→特征衍生→多维探索→静态可视化→交互式拖拽大屏,覆盖大一数据分析全部核心知识点。数据集公开易得,代码可直接运行,课程报告、期末大作业、个人练手都非常合适。
配套资源:数据集可在 Kaggle 搜索
120 years of Olympic history: athletes and results下载,完整代码已全部贴出,复制即可运行。
标签
#Python 数据分析 #Pandas 实战 #Pyecharts 可视化 #奥运数据分析 #数据大屏 #大一课程作业 #Matplotlib #数据挖掘
更多推荐
所有评论(0)