一、前言

大家好,今天分享一套大一课内数据分析完整实战项目 ——1896-2016 百年奥运数据探索与体育强国分析,基于 Kaggle 经典奥运数据集,完整覆盖数据读取、多表关联、缺失值清洗、特征工程、多维探索分析、中国专题、交互式可视化大屏全流程,适合数据分析入门练手、课程大作业、期末报告。

项目信息

  • 数据集:Kaggle 120 年奥运历史数据(athlete_events.csv + noc_regions.csv)
  • 数据规模:27 万 + 运动员参赛记录
  • 技术栈:Pandas、NumPy、Matplotlib、Pyecharts
  • 项目周期:课内 4h + 课外自主实践
  • 学习目标:掌握多表合并、缺失值处理、BMI 特征衍生、静态 + 交互式可视化、拖拽式数据大屏开发

二、数据集介绍

1. 两张核心 CSV

  1. athlete_events.csv 运动员明细表 | 字段 | 含义 | | ---- | ---- | | ID | 运动员唯一编号 | | Name、Sex、Age、Height、Weight | 姓名、性别、年龄、身高 (cm)、体重 (kg) | | NOC | 国家奥委会三位代码 | | Games、Year、Season、City | 赛事全称、年份、夏 / 冬奥、举办城市 | | Sport、Event | 大项、细分小项 | | Medal | 奖牌:Gold/Silver/Bronze/ 空(无奖牌) |

  2. noc_regions.csv 国家代码映射表

  • NOC:奥委会编码
  • region:国家 / 地区全称
  • notes:备注(历史政权、特殊代表团说明)

2. 数据痛点

  1. 大量身高、体重、年龄缺失;
  2. Medal 字段空值代表未获奖,不能直接删除;
  3. 历史政权代码冗余:URS (苏联)、EUN (独联体)、RUS (俄罗斯);GER/FRG/GDR (两德);
  4. 特殊 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 缺失值填充策略
  1. region 空值填充为 Unknown(难民 / 未知代表团)
  2. Medal 空值统一标记No Medal,区分获奖 / 未获奖
  3. 身高、体重、年龄缺失:身高用中位数(不受极端身高干扰),年龄、体重用均值

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'] 东德、西德、统一德国合并统计
策略思考题解答
  1. 统一合并 URS/EUN/RUS 统计总奖牌优势:无需手动拼接多段政权数据,直接按 region 分组即可得到俄罗斯全历史奖牌;
  2. 单独分析俄罗斯联邦:增加过滤条件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("固化大屏完成!")

大屏使用步骤

  1. 运行代码生成奥运大屏草稿.html,Chrome 浏览器打开;
  2. 拖拽、缩放所有图表,自定义大屏布局;
  3. 点击页面左上角Save Config,自动下载chart_config.json
  4. 执行固化代码,生成无多余边框、固定布局的正式大屏 HTML。

四、核心分析结论(写报告直接复制)

1. 全球运动员画像

  1. 百年奥运男性参赛占比 72.5%,女性仅 27.5%,但女性参赛比例持续逐年上升,体现全球性别平等进程;
  2. 男性 BMI 整体高于女性,篮球、举重选手 BMI 显著高于体操运动员;
  3. 运动员平均年龄稳定在 24-26 岁区间,无明显高龄化 / 低龄化趋势。

2. 世界体育版图地缘变化

  1. 全历史榜单:美国、俄罗斯(含苏联、独联体)、德国长期稳居奖牌前三;
  2. 1994 年后现代格局:苏联解体后俄罗斯奖牌总量大幅下滑,中国稳步上升,挤进世界第一梯队;
  3. 长尾效应明显:超半数国家仅获得 1-3 枚奖牌,多为小型岛国、发展中国家。

3. 中国奥运崛起核心发现

  1. 爆发拐点:1984 年洛杉矶奥运会实现金牌零突破,2008 北京东道主奖牌达到峰值;
  2. 性别结构:早期女子奖牌占比极高(跳水、乒乓球、举重),近年男子项目成绩稳步提升,男女发展趋于均衡;
  3. 优势项目:跳水、乒乓球、体操、举重、射击是中国夺金基本盘,符合 “二八定律”,80% 金牌来自 20% 优势项目;
  4. 冬季奥运起步晚,但短道速滑逐步成为冬奥核心夺金项目。

4. 社会学验证结论

  1. 东道主效应:主办国当年奖牌数显著高于前后两届,主场优势客观存在;
  2. 女性平权:1896 首届奥运无女性参赛,2016 里约女性参赛占比突破 40%,是全球女性权益发展的缩影;
  3. 政权更迭直接改变奖牌榜单格局,奥运数据是地缘政治、国家综合实力的直观镜像。

五、项目拓展思考题(课程作业加分项)

  1. 为什么身体指标缺失值身高用中位数、体重 / 年龄用均值?
  2. 统一 URS/EUN/RUS 为 Russia 分组统计有什么优缺点?单独分析俄罗斯联邦如何过滤年份?
  3. 数据集查询首金与历史许海峰是否一致?数据偏差来源是什么?
  4. 如何设计帕累托图验证 “80% 奖牌来自 20% 优势项目”?
  5. 如何通过 City、Year 字段自动识别每届东道主,量化验证东道主光环?

六、项目总结

本项目完整复刻企业级数据分析流水线:数据加载→多表关联→清洗补全→特征衍生→多维探索→静态可视化→交互式拖拽大屏,覆盖大一数据分析全部核心知识点。数据集公开易得,代码可直接运行,课程报告、期末大作业、个人练手都非常合适。

配套资源:数据集可在 Kaggle 搜索120 years of Olympic history: athletes and results下载,完整代码已全部贴出,复制即可运行。

标签

#Python 数据分析 #Pandas 实战 #Pyecharts 可视化 #奥运数据分析 #数据大屏 #大一课程作业 #Matplotlib #数据挖掘

更多推荐