用Python分析奥运会120年数据:数据清洗与可视化实战
2026/9/16 7:24:06 网站建设 项目流程

1. 项目概述与数据准备

1.1 为什么选奥运会数据做分析项目

先讲个实在的:市面上的数据分析教程,用的都是鸢尾花、泰坦尼克号、房价预测这些经典数据集。数据干净、字段简单、拿来练手没问题,但做完之后你会有一种“道理我都懂,轮到自己就不会”的感觉。奥运会120年历史数据集恰好是那个能打破这种感觉的项目。

为什么这么说?因为这份数据集的真实程度远超一般教学数据。从1896年雅典第一届现代奥运会,到2016年里约奥运会,跨越两个甲子的真实比赛记录,包含27万名运动员的参赛信息、赛事项目、奖牌归属和各种身体指标。里面的问题多到你想不到:有的年份数据缺失、有的项目改名、有的运动员年龄明显异常、不同国家的英文名称不统一……这些脏数据恰恰是实际工作中最常见的场景,也正因为如此,处理起来反而比“干净”的玩具数据集更有练手价值。

这个项目我用Python跑过好几遍,每次都有新发现。比如早期奥运会的数据特别稀疏——1896年只有176个人参赛,到2016年里约就是11000多人。怎么处理不同年份数据量差异巨大的问题、怎么从历史演变中提取有价值的信息,都是能写进简历的真实项目经验。如果你正在学Python数据分析,或者准备应聘数据分析岗位,这个项目可以完整覆盖你从数据清洗、探索性分析到可视化呈现的全过程。

1.2 数据集字段与整体概况

先摸清底细。这份120年奥运会数据集,最常用的是Kaggle上开源的版本athlete_events.csv,包含27万+条记录,每条记录是一个运动员在某届奥运会上参加某个项目的一条参赛数据。字段设计比较标准,整理如下:

字段含义数据类型备注
ID运动员唯一编号int一个运动员有多条记录,按参赛项目区分
Name运动员姓名str可能重名,需结合ID判断
Sex性别strM/F
Age年龄float存在异常值,如1岁、95岁
Height身高float单位cm,存在大量缺失
Weight体重float单位kg,存在大量缺失
Team代表队str国家或地区,注意历史政治变迁
NOC国家奥委会代码str用代码统一国家信息
Games届数str如2016 Summer
Year年份int实际举办年份
Season季节strSummer/Winter
City举办城市str便于分析东道主效应
Sport运动大项str如Basketball
Event比赛小项str如Basketball Men's Basketball
Medal奖牌strGold/Silver/Bronze/NA

第一眼看到这个表,你可能会觉得字段也不算特别多。但关键问题在于:这份数据不是为分析准备的,而是从官方记录里直接导出来的。所以缺失值、重复项、格式不统一的问题到处都是。我最开始跑df.info()的时候,Age、Height、Weight三个字段的缺失值都相当可观,尤其是早期奥运会的记录,当时连成绩单都是手写的,很多数据根本没有电子化。

1.3 分析目标与预期产出

拿一份数据上手,最忌讳的就是没有目标直接开跑。我给自己定的几个分析方向,也是建议你参考的框架:

第一个方向:金牌榜的百年变迁。哪些国家长期霸榜?中间有没有被反超的节点?这和当时的经济、政治、体育政策有没有对应关系?这种时间序列分析最能锻炼数据聚合和趋势解读的能力。

第二个方向:运动员身体形态的演变。一百多年前的运动员和现在的运动员,身高、体重、BMI有没有显著变化?不同运动项目的身体门槛是不是越来越分化?比如体操运动员和篮球运动员的身高差是不是在拉大?这一类分析能用到大量统计方法,做出来也很有意思。

第三个方向:性别平等的进程。女性运动员占比从1896年的0%一路爬升到2016年的45%,每个奥运周期增长多少?哪些项目最早向女性开放?田赛、径赛、水上项目分别是什么时候有女选手的?

第四个方向:东道主效应验证。举办奥运会的国家,在当届是不是真的能拿更多奖牌?这个效应能持续多久?是只有东道主受益还是整个地区都受益?

这四个方向做下来,你会发现Pandas的groupbymergepivot_table全都能练到,Matplotlib和Seaborn的可视化技巧也会相当熟练。等项目完成,你产出的不仅仅是一堆图表,而是一条完整的叙事线——用一个半世纪的数据,向观众讲述现代奥运会到底经历了什么。

2. Python环境搭建与工具选型

2.1 一套直接能用的开发环境

很多新手在环境配置这一关就放弃了,其实完全没必要怕。我用过纯命令行、Anaconda,也试过各种IDE,最后稳定下来的一套组合是:Python 3.9+、Jupyter Notebook、Pandas 2.0+、Matplotlib、Seaborn、Plotly

如果你还没装Python,优先装Anaconda而不是裸的Python解释器。原因很简单:Anaconda自带conda包管理器,能帮你省掉无数依赖冲突的坑。在Anaconda Prompt里执行:

conda create -n olympics python=3.9 conda activate olympics conda install pandas matplotlib seaborn jupyter

创建独立环境这个步骤很多人会跳过,但我建议别偷懒。数据分析的项目依赖越来越重,今天装个这个库、明天装个那个库,全堆在base环境里,迟早出问题。独立的conda环境相当于给你的项目隔离了一个干净的容器,装坏了直接删掉重建,不会连累系统Python。

装完基础库之后,再确认一下版本。我用到的关键库和版本号如下:

import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns print(pd.__version__) # 2.0.3 print(np.__version__) # 1.24.3 print(matplotlib.__version__) # 3.7.2 print(sns.__version__) # 12.2

2.2 为什么选这几个库而不是别的

每一层工具的选择都是有理由的,不是看哪个火就用哪个。

Pandas是绝对的核心。没有它的话,处理27万条记录、按年份和国家做分组聚合,你得写几十行循环,想想就头疼。Pandas的groupbypivot_table就是为这种结构化数据量身定做的,一行代码完成按年份+国家+奖牌的三层聚合,性能也完全扛得住这种量级的数据。

Matplotlib负责打底。它是Python可视化的地基,seaborn和plotly都是建在它上面的。虽然Matplotlib的默认样式看起来有点老气,但它的控制粒度是最细的,想调整任何细节都有对应的API。我在实际项目里通常用Matplotlib出最终的论文级图表,用Seaborn做探索性的快速绘图,两者配合效率很高。

Seaborn用来画统计图表。它的优势在于一行代码就能出分布图、热力图、聚类图,而且自带配色比Matplotlib默认的好看太多。奥运会数据里大量涉及分组比较(不同年份、不同国家的指标对比),Seaborn的boxplotviolinplotbarplot用起来非常顺手。

Plotly做交互图表。静态图发文章可以,但做数据探索和分析演示时,交互式图表的效果好得多——鼠标悬浮看具体数值、拖拽缩放、点击图例筛选数据,这些功能对分析非常有帮助。尤其是对比120年间各国奖牌数变化,交互式折线图能让你快速定位每个波峰波谷对应的年份。

2.3 环境踩坑记录

这里分享三个我实际踩过的坑,希望你能绕开:

第一个坑是Matplotlib中文乱码。默认情况下Matplotlib不支持中文标签,直接画会出现方块。需要在绘图前设置中文字体:

plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False

第二个坑是Pandas版本升级导致的API变化。老版本的df.append()在新版本里被移除了,必须改用pd.concat()。很多老教程还在用旧API,照抄会报错。所以遇到报错时,先看看是不是API变了。

第三个坑是行数显示不全。Jupyter里默认只显示部分行,分析120年数据时往往需要看全貌:

pd.set_option('display.max_rows', None) pd.set_option('display.max_columns', None)

3. 数据清洗与预处理

3.1 加载数据与初步探查

数据清洗是数据分析里最枯燥但是最重要的一步。直接上代码:

import pandas as pd df = pd.read_csv('athlete_events.csv') print(df.shape) # (271116, 15) print(df.head(3)) print(df.dtypes) print(df.isnull().sum())

输出结果会告诉你几件事:总共有27万多条记录、15个字段,Age、Height、Weight、Medal四个字段存在缺失值。注意Medal的缺失值并不是“漏填”,而是代表该运动员当届未获奖牌——这个语义区别非常重要,后续分析时必须把“没有奖牌”和“数据缺失”区分开处理。

3.2 异常值与重复值处理

很多新手拿到数据就开始画图,画完发现结论很怪,回过头来才发现是清洗没做好。我整理了几类必须处理的问题。

异常年龄问题。原数据里Age最小是1(运动会婴儿?)、最大是95(这也不太符合常理)。我当时的处理方案是限定Age在10到80之间:

df = df[(df['Age'] >= 10) & (df['Age'] <= 80)]

要注意的是:这里不是简单地删除,而是要考虑业务合理性。理论上确实有人接近80岁还在参加射击、帆船等对体能要求较低的项目,所以上限设宽一点。但如果只做整体年龄段分析,超出这个范围的样本量极少,剔除后对结论方向没有实质影响。

重复值问题。同一个运动员参加多个项目、多届奥运会,在数据里就是多行。行本身并不重复,但ID+Event+Games完全一样的记录可能是真正的重复。可以用:

duplicated_rows = df[df.duplicated(subset=['ID', 'Games', 'Event'], keep=False)]

检查完后,如果确认是同一运动员在同一届同一项目的重复录入,直接drop_duplicates去掉。

身高体重的极端值。我画了箱线图之后发现,有些篮球运动员身高在175cm以下,这不一定是错误——可能是录入时把英尺和厘米搞混了,也可能是女子运动员真实数据。这种情况不能一刀切删除,建议结合具体项目判断。如果你只想做整体趋势分析,可以按项目分组后用Z-score方法剔除明显超过合理范围的离群点:

from scipy import stats df['Height_zscore'] = df.groupby(['Sport'])['Height'].transform( lambda x: (x - x.mean()) / x.std() ) df_height_clean = df[abs(df['Height_zscore']) < 3]

3.3 缺失值的处理策略

缺失值处理没有银弹,完全取决于字段和分析目标。

Medal字段。它的缺失代表“没获奖”,是真实信息而非缺失数据。分析获奖率时,需要把NaN填充为'None':

df['Medal'] = df['Medal'].fillna('None')

Height和Weight字段。缺失比例不低,早期奥运会尤其严重。如果你的分析聚焦在身体指标变化趋势,可以用整体均值填充,但更好的做法是按项目+性别分组填充均值——因为不同项目运动员的身高体重差异巨大,篮球运动员和马拉松运动员完全不是一个量级。用整体均值填充会把所有项目拉向一个中间值,严重扭曲分析结果:

df['Height'] = df.groupby(['Sport', 'Sex'])['Height'].transform(lambda x: x.fillna(x.mean())) df['Weight'] = df.groupby(['Sport', 'Sex'])['Weight'].transform(lambda x: x.fillna(x.mean()))

Age字段。缺失的比例不算特别大,我直接删除缺失行,因为年龄是分析性别平等、身体形态演变时的核心变量,占比不到2%的缺失样本删掉即可,不影响整体结论。

3.4 新增特征:BMI与奖牌权重

清洗完原始字段后,可以基于领域知识构造几个新特征,让分析更深入。

BMI(身体质量指数)。有身高和体重就能算,公式是体重(kg)/身高(m)的平方。BMI在不同运动项目间的差异很有意思——马拉松选手普遍在20以下,柔道、举重选手可能会超过28。分析BMI随年份的变化趋势,能反映运动科学的发展轨迹:

df['BMI'] = df['Weight'] / (df['Height'] / 100) ** 2

奖牌权重。分析国家体育实力时,单纯数金牌数量会忽略银牌和铜牌的价值。我自定义了一个加权得分体系:金牌3分、银牌2分、铜牌1分。这个不是唯一标准,但做时间序列趋势分析时比单纯计数更能反映综合实力。

参赛与获奖标志。给每条记录加一个字段标记运动员是否获得奖牌,方便后续按获奖率分析。

4. 核心分析维度实战

4.1 历年参赛规模与项目扩张

先看整体趋势。用一行代码聚合出每届奥运会的参赛运动员数量:

participants_by_year = df.groupby(['Year', 'Season'])['ID'].nunique().reset_index() participants_by_year = participants_by_year.rename(columns={'ID': 'Athlete_Count'})

画出图之后,你能直观看到两个显著节点:一是二战后参赛人数快速攀升;二是1990年代后职业运动员被允许参赛,人数再次暴涨。还有一个有趣的现象是受两次世界大战影响,1916年、1940年、1944年奥运会直接取消,表现在图上就是三个断档期。

同一时期,比赛小项的数量也在高速膨胀。1896年只有43个小项,2016年已经超过300个。这就引出一个问题:拿奥运金牌的含金量到底变没变?这些问题可以用代码来量化分析:

events_by_year = df.groupby(['Year', 'Season', 'Sport'])['Event'].nunique().reset_index() events_by_year = events_by_year.rename(columns={'Event': 'Event_Count'})

4.2 百年金牌榜演变与霸主更替

国家奖牌榜是大众最关注的分析,也是最能体现数据聚合能力的一个模块。写起来其实直接:

medals = df[df['Medal'] != 'None'] team_medals = medals.groupby(['Year', 'NOC', 'Medal']).size().reset_index(name='Count') team_pivot = team_medals.pivot_table(index='NOC', columns='Medal', values='Count', fill_value=0) team_pivot['Total'] = team_pivot['Gold'] * 3 + team_pivot['Silver'] * 2 + team_pivot['Bronze'] team_pivot_sorted = team_pivot.sort_values('Total', ascending=False).head(10)

做完这个表之后,可以进一步按年份拆分,画出几个体育强国奖牌数的时间序列。你会看到明显的阶段性规律:早期金牌高度集中在少数几个国家,随着奥运会扩张,奖牌开始分散化,新兴体育国家陆续崛起。这里面还能继续做东道主效应的量化分析:

# 找到所有东道主国家 hosts = df.groupby(['Year', 'City'])['NOC'].agg(lambda x: x.value_counts().idxmax()).reset_index() # 合并奖牌数据看东道主当届与前后届表现差异

4.3 运动员身体形态百年进化

运动员身体指标的变化是整个项目中最“硬核”的分析维度,因为它能直接反映运动科学的进步。

用Seaborn画一个按年代划分的身高分布箱线图,你会看到两个趋势:整体身高在缓慢上升,但不同项目的分化在加剧。篮球、排球运动员的身高增长远快于马拉松、体操项目。

df['Decade'] = (df['Year'] // 10) * 10 plt.figure(figsize=(14, 6)) sns.boxplot(data=df[df['Sport'].isin(['Basketball', 'Gymnastics', 'Athletics'])], x='Decade', y='Height', hue='Sport') plt.title('不同项目运动员身高随年代的变化') plt.xticks(rotation=45) plt.show()

BMI的分析同样有价值。我分析后发现一个规律:投掷类项目的BMI中位数逐年上升,而长跑项目的BMI中位数基本稳定或略有下降。这说明运动选材越来越精细——每个项目都在寻找最适合自身特点的身体形态。

4.4 性别平等的历史进程

女性参与奥运会的历程,本身就是一个精彩的数据叙事。直接用性别分组聚合:

gender_by_year = df.groupby(['Year', 'Sex'])['ID'].nunique().reset_index() gender_pivot = gender_by_year.pivot(index='Year', columns='Sex', values='ID').fillna(0) gender_pivot['Female_Ratio'] = gender_pivot['F'] / (gender_pivot['M'] + gender_pivot['F'])

绘图后你会发现,女性参赛比例从1900年的2.2%左右,一路波动上升到2016年的45.6%。中间有几个加速节点:1912年游泳项目对女性开放,1928年田径项目对女性开放,1976年之后女性参赛比例稳定爬升。

另一个值得关注的视角是女性何时开始获得金牌。第一批女冠军出现在1900年,参加的是网球和高尔夫项目——这两个项目在当时被认为是“适合女性”的。这个分析结果能引出很多社会层面的讨论,用在项目汇报里面非常出彩。

4.5 东道主效应验证

关于东道主效应,体育界一直有两种争论:有人认为主场作战优势巨大,有人认为现代交通条件下主场优势已经弱化。我们用120年数据来检验。

我的做法是:先建立国家队伍成绩的时间序列,然后标记出该国举办奥运会的年份,比较该年份前后各一届的成绩:

def get_host_years(df): host_records = df.groupby(['Year', 'City']).apply( lambda x: x['NOC'].mode().iloc[0] ).reset_index() host_records.columns = ['Year', 'City', 'Host_NOC'] return host_records host_years = get_host_years(df)

分析结果显示,东道主效应在多数国家是真实存在的,但强度因国而异。体育大国作为东道主时,奖牌数涨幅相对有限——因为基数已经很高,上升空间有限。体育小国作为东道主的涨幅惊人,有时能达到前届的4-5倍。这个结论有一定普适性,适用于大多数举办国。

5. 数据可视化与报告呈现

5.1 静态图表的进阶技巧

Matplotlib和Seaborn能画出90%你需要的图表,关键是掌握几个细节。

配色方案。不要用默认的彩色,选一个统一的配色方案会专业很多。我用的是Seaborn自带的deeppastel配色,或者直接用颜色代码自定义。分析金牌榜时,我用深金色突出第一名,其他名次用渐变色,视觉层次一下就出来了。

字体设置。中文字体显示问题处理好了之后,图表专业感也会提升不少。

图例和图标题。标题要包含结论性的信息。比如“女性参赛比例从1900年的2.2%升至2016年的45.6%”,这比“女性参赛比例趋势图”传递的信息量大得多。数据分析图表的目的是沟通结论,不是展示绘图技术。

5.2 交互式图表与演示

静态图适合写报告,但做分析探索或者向团队展示时,Plotly的交互图表效果好得多。画一个各国金牌数随年份变化的交互折线图:

import plotly.express as px top_countries = ['USA', 'URS', 'GBR', 'CHN', 'FRA', 'ITA', 'GER', 'AUS'] df_top = df[df['NOC'].isin(top_countries) & (df['Medal'] == 'Gold')] gold_by_year = df_top.groupby(['Year', 'NOC']).size().reset_index(name='Gold_Count') fig = px.line(gold_by_year, x='Year', y='Gold_Count', color='NOC', title='主要国家历年金牌数量变化') fig.show()

鼠标悬浮能看到具体年份和金牌数,点击图例可以隐藏/显示某个国家,拖拽可以放大某个时间段。用来做分析演示的时候,这种交互体验比静态图强很多。

5.3 用一组图表讲一个完整故事

数据分析产出的不是孤立的图表,而是一套叙事。我这个项目最终选了四张图组成核心报告:

第一张:历年参赛人数与女性占比的双轴图。一条线是参赛总人数,另一条线是女性参赛比例,两个指标放在同一张图上,展示现代奥运会从精英赛事到全民盛事、从男性垄断到性别平等的双重变迁。

第二张:金牌榜前三名的国家变化面积图。用堆叠面积图展示不同时期金牌榜的霸主更替,观众一眼能看到“谁在哪个年代统治了奥运赛场”。

第三张:不同运动项目BMI分布的对比小提琴图。横向对比各项目运动员的BMI中位数和分布形状,清晰揭示不同运动的身体条件差异。

第四张:东道主效应前后对比柱状图。选取几个典型的东道主国家,并排展示他们举办前一届、举办当届、举办后一届的奖牌数,直观展示主场效应的强度与消退速度。

这四张图不用过多文字解释,看图的人就能自动得到一个完整的百年奥运史叙事。这就是数据可视化应该达到的效果——不是展示数据,而是讲述故事。

6. 完整项目代码与执行流程

6.1 一键跑通的完整代码

前面拆开了讲各个模块,这里给一个可以一键跑通的完整脚本框架。我把整个分析流程串起来,你把这套代码保存为olympics_analysis.py,按顺序运行即可:

# -*- coding: utf-8 -*- """ 基于Python的奥运会120年历史数据分析 运行环境:Python 3.9 / Pandas 2.0 / Matplotlib 3.7 / Seaborn 12.2 """ import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 1. 数据加载 df = pd.read_csv('athlete_events.csv') print(f"数据总量: {df.shape[0]} 条, 字段数: {df.shape[1]}") # 2. 数据清洗 df = df[(df['Age'] >= 10) & (df['Age'] <= 80)] df.drop_duplicates(subset=['ID', 'Games', 'Event'], keep='first', inplace=True) df['Medal'] = df['Medal'].fillna('None') # 3. 缺失值处理(按项目+性别分组填充) df['Height'] = df.groupby(['Sport', 'Sex'])['Height'].transform( lambda x: x.fillna(x.mean())) df['Weight'] = df.groupby(['Sport', 'Sex'])['Weight'].transform( lambda x: x.fillna(x.mean())) df.dropna(subset=['Age'], inplace=True) # 4. 特征工程 df['BMI'] = df['Weight'] / (df['Height'] / 100) ** 2 df['Decade'] = (df['Year'] // 10) * 10 # 5. 分析1:历年参赛规模 participants = df.groupby('Year')['ID'].nunique().reset_index() participants.columns = ['Year', 'Athlete_Count'] # 6. 分析2:女性参赛比例 gender = df.groupby(['Year', 'Sex'])['ID'].nunique().reset_index() gender_pivot = gender.pivot(index='Year', columns='Sex', values='ID').fillna(0) gender_pivot['Female_Ratio'] = gender_pivot['F'] / (gender_pivot['M'] + gender_pivot['F']) # 7. 分析3:金牌榜TOP10 medals = df[df['Medal'] != 'None'] team_medals = medals.groupby(['Year', 'NOC', 'Medal']).size().reset_index(name='Count') team_pivot = team_medals.pivot_table(index='NOC', columns='Medal', values='Count', fill_value=0) team_pivot['Total'] = team_pivot.get('Gold', 0) * 3 + \ team_pivot.get('Silver', 0) * 2 + \ team_pivot.get('Bronze', 0) top10 = team_pivot.sort_values('Total', ascending=False).head(10) # 8. 分析4:BMI分布随年代变化(抽样以避免绘图过慢) sample = df[df['Medal'] != 'None'].sample(min(20000, len(df)), random_state=42) # 9. 可视化输出 fig, axes = plt.subplots(2, 2, figsize=(16, 12)) # 图1:参赛规模变化 axes[0, 0].plot(participants['Year'], participants['Athlete_Count'], marker='o', linewidth=1.5) axes[0, 0].fill_between(participants['Year'], participants['Athlete_Count'], alpha=0.3) axes[0, 0].set_title('历届奥运会参赛运动员数量变化') axes[0, 0].set_xlabel('年份') axes[0, 0].set_ylabel('参赛人数') # 图2:女性参赛比例 axes[0, 1].plot(gender_pivot.index, gender_pivot['Female_Ratio'] * 100, 'r-', linewidth=2) axes[0, 1].set_title('女性参赛比例历史变化') axes[0, 1].set_xlabel('年份') axes[0, 1].set_ylabel('女性占比 (%)') # 图3:金牌榜TOP10 top10_sorted = top10.sort_values('Total', ascending=True) axes[1, 0].barh(top10_sorted.index, top10_sorted['Total'], color='steelblue') axes[1, 0].set_title('历史金牌总数TOP10国家/地区') # 图4:BMI分布 sns.boxplot(data=sample[sample['Sport'].isin(['Basketball', 'Gymnastics', 'Swimming'])], x='Decade', y='BMI', hue='Sport', ax=axes[1, 1]) axes[1, 1].set_title('不同项目BMI随年代变化的分布') axes[1, 1].tick_params(axis='x', rotation=45) plt.tight_layout() plt.savefig('olympics_overview.png', dpi=150) plt.show()

这段代码整合了前面讲的核心清洗步骤和四类典型分析,跑完后会输出四张分析图。建议你在自己的数据集上跑一遍,然后根据实际输出结果调整分析参数和可视化细节。

6.2 代码性能优化笔记

27万条记录不算大,但如果不注意写法,某些操作也可能变得很慢。分享几个性能优化经验:

groupby+transform比循环快得多。填充缺失值时,用循环按组处理可能要跑几十秒,用transform是毫秒级。这个差距在大数据量时会被放大,推荐养成用transform的习惯。

数据量过大时先采样再可视化。画散点图、箱线图时,如果数据点超过5万,绘制会比较耗时,而且图上的点会叠成一团看不清。先随机采样再画图,既保证速度又不影响结论:

sample_for_plot = df.sample(n=10000, random_state=42)

pivot_table是分类汇总利器。相比多次groupby再合并,pivot_table能直接按行列同时聚合,代码简洁度和执行效率都好很多。

6.3 项目扩展方向

基础版本做完之后,这个项目还能往多个方向扩展:

加入机器学习预测。用历史数据训练一个模型,根据运动员的项目、年龄、身高、体重等指标预测获奖概率。逻辑回归或者随机森林都能达到不错的效果,可以对比不同模型的表现。这个扩展方向能让你从数据分析进阶到建模,简历上更好看。

引入外部数据。把各国GDP、人口、气候等宏观数据融合进来,分析体育成绩与国家发展水平的关系。这需要学习数据合并和关联分析,也是数据分析实际工作中的高频场景。

爬虫扩展。从官方网站抓取2020东京和2024巴黎的最新数据,补充到历史数据集中。这部分能锻炼爬虫能力,还能让你体验一次从数据采集到分析的全流程。

搭建可视化Web应用。用Streamlit或Dash把分析结果包装成一个交互式Web应用,让非技术用户也能自己探索数据。这个扩展方向的完成度较高,对求职展示很有帮助。

7. 常见问题速查与避坑指南

7.1 高频问题解决方案

数据分析和代码运行过程中,有些问题几乎每个做这个项目的人都会遇到。我整理了一个问题速查表,按出现频率排序:

问题原因解决方案
导入CSV时报UnicodeDecodeError文件编码不是UTF-8改为pd.read_csv('athlete_events.csv', encoding='latin1')
图表中文显示为方块Matplotlib默认字体不含中文设置plt.rcParams['font.sans-serif'] = ['SimHei']
负号显示为方块中文字体缺少数学符号同时设置plt.rcParams['axes.unicode_minus'] = False
年份显示成了浮点数有缺失值导致整列转float读取时指定dtype={'Year': int},或清洗后再转换
奖牌统计翻倍同一运动员参加多个小项Event去重,或明确统计口径
内存不足读入后未释放中间变量del删除不再使用的DataFrame,或分块读取

这类项目最容易出问题的地方不在代码本身,而在于统计口径。比如“某个国家一共拿了多少奖牌”,到底按参赛记录数还是按获奖人数数,结果差很多。做之前先想清楚口径定义,能省掉后面很多返工。

7.2 分析结论的可靠性判断

这部分是很多自学者容易忽略的。拿到分析结果后,不要急着下结论,先问自己三个问题:

结论有没有受到异常值的影响?比如分析平均年龄时,如果某届混进了一个95岁的运动员,均值就会被拉高。建议先看分布图,再做统计推断。

数据缺失对结论方向有没有系统性影响?早期奥运会缺失数据较多,直接用不同年份的数据做对比时,要意识到缺失本身就是一种偏差。如果某年数据只记录了部分项目,那一年算出来的参赛人数天然偏低,这个结论不是“当年真的那么少”,而是“我们只知道那么多”。

相关关系不等于因果关系。比如我们发现人才培养体系完善的国家体育成绩更好,但“完善体系”和“好成绩”之间的因果关系并不是单方向的。报告里要避免“因为A所以B”的表述,改成“A与B存在正向关联”更严谨。

7.3 独家避坑经验

最后分享几个我反复碰壁之后才明白的经验,这些细节在官方文档里看不到:

保存清洗后的数据。清洗完数据后先存一份副本:

df.to_csv('athlete_events_clean.csv', index=False)

这样做的好处是:后续重新打开项目时,不用再跑一遍整个清洗流程,直接读副本就行。而且万一某个清洗步骤写错了,你能从原数据重新来,不用从头刷新。

分组聚合时关注一下observed参数。新版Pandas中,如果分组列是category类型,groupby默认只统计被观察到的类别。这个细节不影响大多数人,但如果你发现聚合结果少了某个类别,不妨检查一下这个参数。

记录每一步分析的决定。比如为什么决定删除Age<10的数据、为什么用分组均值填充缺失值。这些决策过程会在项目复盘或者面试时成为你的谈资,体现出你的数据思维。

不要在一个脚本里写所有分析。把项目拆成多个Jupyter Notebook或多个Python脚本,每个模块职责单一——数据清洗、分析计算、可视化、报告,分开管理。改起来方便,定位问题也快,到最后做演示时再整合成一个完整报告。

8. 项目学习的深层收获

做这个数据分析项目,最终收获的绝不只是几行代码和几张图表。我在反复调试和思考的过程中,真正理解了数据分析岗位的核心能力模型——不是操纵工具,而是用数据回答问题的思维能力。面对120年奥运会数据,你需要考虑选择分析哪个维度、怎么定义奖牌竞争力、如何处理缺失的历史记录、怎样可视化才能让观众一眼抓住规律,这些问题没有标准答案,都需要你基于对体育和数据的理解做出判断。

如果你想用这个项目来准备面试,建议把分析报告做好之后,再准备几个延伸问题:分析结论的局限性是什么?如果数据源换成了其他运动会,这套分析流程能用吗?如果想引入新的外部数据,你打算怎么做?把这些问题想透,你对该项目的掌控力会明显提升。

这个项目后续还可以沿着很多方向继续做下去。我曾尝试把123年(截至2024巴黎)的数据全部纳入,比较不同世纪运动员身体指标的变化速度;也尝试构建一个能预测下届奥运会奖牌榜的简易模型。每一次扩展,都会发现自己对数据、对体育、对历史的理解更深了一层。如果你也做出了属于自己的版本,欢迎分享你发现的有趣规律。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询