ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Python的奥运会历史数据可视化系统:从清洗到交互看板

基于Python的奥运会历史数据可视化系统:从清洗到交互看板

1. 为什么我会想做一个奥运会数据可视化系统

先说个实际场景。前两年每次奥运会结束,社交媒体上都会刷屏各种“奖牌榜”。但大家讨论的也就是“金牌数多少、排第几”,很少有人去追问:**这个国家的奖牌数,放在过去一百多年里到底处于什么位置?这个项目从来拿不到牌到突然崛起,是哪一届、哪个项目推动的?东道主到底有没有“主场红利”?**这些问题靠记忆和感觉是答不出来的。

我当时正好手边有一份流传较广的历届奥运会历史数据集,字段挺完整,从运动员姓名、性别、年龄、身高体重,到参赛国家代码、届次、年份、季节、城市、大项、小项、奖牌类型都有。数据量大概几十万条,单用 Excel 打开就很吃力了,更别提跨届次做对比分析。

想了几天,我决定直接做一个基于 Python 的可视化分析系统,把整个流程跑通:数据清洗、特征分析、聚合计算、交互式图表、最后用一个 Web 界面来承载所有分析结果。做完了我才发现,这个项目真正的价值不在“画了哪些图”,而在于把一堆看似杂乱的历史数据,整理成一套能自由探索的决策看板。如果你也想练习 Python 数据分析,或者想找一个能写进简历的完整项目,这篇内容应该能帮你少走不少弯路。

我接下来会按我自己实际开发的顺序来讲,先说数据集本身,再说清洗的细节,然后是架构选型、核心分析维度,最后是开发中踩到的真实坑。尽量把每个关键选择背后的理由也讲清楚。

2. 数据源与字段结构:先搞清楚手上有什么牌

2.1 历史奥运会数据集的基本情况

这份数据覆盖了从 1896 年雅典奥运会到 2016 年里约奥运会的所有夏季、冬季奥运会记录,按运动员维度展开,每条记录代表一位运动员在某届奥运会中参加的一个项目。注意这里说的“一条记录”是“参赛项目级”,不是一个运动员一行——一个人参加了三个项目,就有三行数据。这个粒度差别很关键,后面做奖牌统计时如果不先做去重,会出错。

字段大体如下:

字段名含义示例
ID运动员唯一标识1
Name运动员姓名A Dijiang
Sex性别M / F
Age参赛年龄24
Height身高(厘米)180
Weight体重(千克)80
Team代表队名称China
NOC国家奥委会代码CHN
Games届次名称2016 Summer
Year年份2016
Season季节Summer
City举办城市Rio de Janeiro
Sport大项Basketball
Event小项Basketball Men's Basketball
Medal奖牌类型Gold / Silver / Bronze / NaN

有几件容易被忽略的事要在做任何分析前想清楚:

  • 冬季奥运会数据也混在里面。Summer 和 Winter 的参赛项目、国家结构差异很大,混在一起做趋势分析会失真。通常先按 Season 拆开,或者用参数控制。
  • 早期奥运会没有完整的奖牌记录,部分运动员年龄、身高体重是空的,这部分缺失对“运动员画像分析”影响很大,但对“国家奖牌趋势”影响不大。
  • Team 字段是国家奥委会的参赛队名称,不等于国籍,一些历史时期的代表队名称和现在不同,比如有些国家在不同届次用了不同的队伍名称,要注意归一化。

我当时的第一步是把数据读进来,看一眼基本信息,确认行数和缺失情况再动手,这个习惯一直保留到现在。

import pandas as pd df = pd.read_csv("athlete_events.csv") print(df.shape) # (行数, 列数) print(df.info()) print(df.isnull().sum())

这条命令输出的缺失值统计基本决定了后续清洗的优先级。奖牌列缺失代表“没获奖”,这个是正常的,不是说数据坏了。

2.2 数据采集的替代方案与成本对比

如果你没有现成数据集,也可以自己写爬虫去公开体育数据库抓。但我的建议是:能不用爬虫就别用爬虫。奥运会历史数据的公开数据集已经比较成熟,质量高、字段全,自己爬反而要处理反爬、动态渲染、历史页面缺失等问题,代价很高。

我当时对比过三种方案:

  • 使用公开 CSV 数据集:最快,直接进入分析阶段,但字段说明需要自己摸索。
  • 调用第三方体育数据 API:数据更新及时,但历史数据往往需要付费,且请求配额有限。
  • 自己开发爬虫:灵活但耗时,适合已有明确目标且公开源无法满足的情况。

最后选了第一种。这个选择的关键不是“省事”,而是把有限的精力放到这个项目的核心价值上——分析逻辑和可视化交互,而不是数据搬运。

3. 数据清洗与预处理:八成的坑都在这一步

3.1 缺失值处理策略,不能一把梭删光

用isnull().sum()看完缺失情况后,很多人第一反应是dropna()把带空值的行全删了。这在大数据处理里是典型的下策。原因很简单:不同字段的缺失对分析的破坏力完全不同。

以这份数据为例:

  • Medal 缺失:表示该运动员没获得奖牌,这是正常状态,不能删。后续做获奖分析时只需筛选非空即可。
  • Age / Height / Weight 缺失:做运动员分布分析时影响大,但做国家奖牌趋势时完全不影响。所以不应该整体删除,而是在不同的分析模块里单独处理。
  • Event 字段早期数据不完整:早期项目设置和现在差异大,如果做“项目数量变化趋势”,要特别小心早期数据的可比性问题。

我当时的清洗逻辑是写一个专门的预处理函数,分字段设置策略,而不是一个dropna()走天下。

def clean_olympic_data(df): df = df.copy() # 奖牌字段空值填为 No Medal,方便后续统计 df["Medal"] = df["Medal"].fillna("None") # 年龄、身高、体重保留空值,在各分析函数内按需过滤 # 处理代表队名称中的历史遗留问题 df["Team"] = df["Team"].str.strip() # 添加一个便于统计的列:是否获奖 df["is_medalist"] = df["Medal"].isin(["Gold", "Silver", "Bronze"]) return df

你可能注意到了,我加了一个is_medalist布尔列。原因是在做“各国获奖人数”“获奖率”这类统计时,普通筛选需要反复写条件,而加这一列后代码可读性会好很多。

3.2 奖牌去重:最容易算错的一类问题

这是整篇文章里最值得警惕的一个坑。前面说过,原始数据是“运动员-项目”粒度,那么一个运动员在同一个大项里拿了两个项目奖牌,就有几条记录。

举个例子:某游泳运动员在同一届奥运会拿了 100 米金牌和 200 米银牌,那么在数据里就是两行,每行分别对应一个 Event。如果你直接按Team + Year + Medal分组计数,得到的是“奖牌项目数”,不是“奖牌数”,因为个人项目确实是一个运动员拿一块奖牌。这里倒不会多算。

真正会出错的地方是团体项目。比如足球队拿了一块金牌,数据里会有 11 名运动员、11 行记录,每行都带Gold。如果你直接逐行计金牌数,会把一块团体金牌算成 11 块。这会让中国、美国这种团体项目强的国家奖牌数严重虚高。

处理团体项目奖牌,业界并没有统一标准,公开数据对应的统计口径一般是“按奖牌事件数据逐项统计”。实际业务中比较稳健的做法是:做国家奖牌数对比时,先按Event + NOC + Year + Medal去重,再计数。

# 按奖项事件去重后再统计国家奖牌数 medal_event_df = df[df["Medal"] != "None"].drop_duplicates( subset=["Year", "Season", "Event", "NOC", "Medal"] ) country_medal = ( medal_event_df.groupby(["Year", "NOC"])["Medal"] .value_counts() .unstack(fill_value=0) )

这样处理后,团体项目的多行记录被折叠为一行,个人项目只占一行,统计结果就比较贴近“一枚奖牌”的物理意义。做排行榜、历史趋势时,我统一用的是medal_event_df。

3.3 国家代码归一化与国籍陷阱

NOC 字段用的是国际奥委会三字代码,比如 CHN、USA、RUS、GER。看起来挺工整,但历史上有几个麻烦:

  • 有的国家在不同历史时期采用不同代码,比如俄罗斯在不同届次用过不同的队伍名和代码。
  • 有些国家联合组队参赛,如 1956 年之前的某些届次,奖牌归属于联合队。直接按 NOC 聚合时会把不同国家的成绩混在一起。
  • 东德、西德在 1968 至 1988 年期间一度联合组队后又是分别组队,名称变化比较多。

我的处理办法是建一个NOC 映射表,把历史上的各种队伍名和代码统一映射到“当前普遍认可的国家或地区”。这个表不依赖任何第三方库,就是手动维护了一组字典,放在配置文件里。这样做确实花了一点时间,但换来了分析结果的可信度。否则做“国家历史奖牌趋势”时,某条曲线会突然断层,原因不是这个国家变弱了,而是代码变了。

清洗完之后,我还做了一步校验:对比几个已知事实,比如 2008 年北京奥运会中国队金牌数是否为 48(按当时普遍统计口径),如果对不上就说明清洗逻辑有问题。这种“以终为始”的校验思路,比反复看数据靠谱得多。

4. 系统架构与可视化选型:Python 全家桶的正确打开方式

4.1 整体架构:分析层与展示层分离

这个系统我没有做成单体应用,而是分成了三条相对独立的链路:

  • 数据处理层:Pandas + NumPy 负责清洗、聚合、分组计算,产出各种 DataFrame。
  • 分析计算层:按分析主题封装 Python 模块,每个模块返回标准化的统计结果和图表数据。
  • 展示层:用 Web 框架做页面和接口,前端通过图表库渲染交互图形。

为什么要分离?核心原因是分析逻辑和展示逻辑的迭代频率完全不同。清洗规则改了,不需要动前端;前端交互改了,不需要重新算数据。对于个人项目来说,这种解耦还能让你在开发不同阶段时只带着少量代码思维工作,压力小很多。

4.2 图表方案对比:我为什么选了 ECharts

可视化方案当时有三个候选,各有优劣:

方案优点缺点适合场景
Matplotlib出图快,适合做静态报告交互性弱,默认样式老旧快速验证数据分布
Plotly交互强,画热力图方便数据量大时页面卡顿独立分析脚本
ECharts(含 pyecharts)交互流畅,中文生态好,主题丰富需要部署前端文件Web 可视化大屏

我最终选择的是 ECharts。原因有几个比较实际:一是它渲染大数据量的点比较多时性能还行;二是中文文档和社区方案多,遇到问题好搜索;三是它默认的交互状态就够用,比如数据缩放、tooltip、图例筛选,不需要自己写太多 JavaScript。

如果你不想碰前后端分离那一套,也可以用 Streamlit 快速搭一个分析应用,组件拖拽式添加图表。但 Streamlit 在自定义页面布局、复杂交互联动时还是不如 ECharts 灵活。我的建议是:如果项目是“给自己用”,Streamlit 足够;如果项目要展示给别人、要做成系统感很强的看板,那还是 ECharts 这种“专业图表库 + Web 框架”的做法更合适。

4.3 数据接口设计:一次计算,多次查询

展示层需要的数据,我都提前在计算层算好,存成 JSON 接口输出,而不是每次请求都重新聚合。比如奖牌趋势接口的结构大概是:

{ "status": "success", "data": { "years": [1896, 1900, ...], "nations": ["USA", "CHN", ...], "medal_counts": { "USA": {"Gold": 100, "Silver": 80}, "CHN": {"Gold": 40, "Silver": 30} } } }

前端拿这个 JSON 直接渲染。优点是响应快、逻辑简单、前端不用关心背后有多少计算;缺点是需要预先定义好接口结构,新增分析维度时要同时改后端和前端。这个“一次计算,多次查询”的思路在做大数据可视化时非常重要,比每次刷新都重跑groupby省太多时间了。

5. 核心分析模块与图表设计

系统里我做了七个分析模块,覆盖了从宏观到微观的各种视角。下面挑几个最有价值的说。

5.1 历年奖牌趋势分析:一眼看出大国兴衰

这个模块的图表默认展示“历史总奖牌数前十国家”的金牌数量趋势,支持点击图例切换国家。

聚合逻辑很简单:按年份和国家分组,统计金、银、铜牌数量,然后取累计奖牌数最多的前十名做趋势线。

值得多说一句的是实现细节:趋势图默认展示的是每个国家的当年奖牌数,而不是累计数。用累计数画出来的线基本只会单调向上,看不出各阶段的波动,对分析没有帮助。当年数的曲线才能反映出某个国家在某个历史阶段的崛起、下滑、恢复。

这个图我会额外配一个数据缩放条(dataZoom),因为时间跨度 120 年,不缩放的话,早期数据点挤在一起根本看不清。

5.2 国家优势项目分析:用热力图找到“传统强项”

第二个核心模块是“国家 × 大项”奖牌热力图。纵轴是国家名,横轴是大项名,格子颜色深浅代表该国家在该大项的奖牌总数。

这个图对发现国家优势项目特别直观。比如能看到某几个国家对篮球、足球、游泳、田径这类大项的垄断程度,也能看到一些小国靠特定小项刷存在感的模式。

实现上用的是 ECharts 的heatmap系列。前端根据经纬度数据自动配色,颜色越深表示奖牌越多。为了让对比公平,我一般展示的是“按金牌数加权后的总分榜”,而不是总奖牌数,因为金牌的历史含金量更高。

5.3 东道主效应分析:主场作战到底有没有红利

这个模块是很多人感兴趣的。如果把东道主举办年的奖牌数,和它前后两届的平均奖牌数对比,能看到一个普遍规律:大多数东道主在举办年奖牌数有明显提升。

不过有一个分析陷阱:单纯看举办年奖牌数高,不能证明“东道主效应”存在,因为国家整体实力可能在上升期。更严谨一点的做法是计算“当年奖牌数相对过去三届均值的增幅”,并和第二名的同期数据对比。我在系统里做了双线对比,一条是东道主,一条是同届非东道主对照组,用相对增幅来展示效应大小。

5.4 运动员画像分析:年龄、身高、体重的时间演化

这一块数据缺失很严重,早期很多运动员没有身高体重数据,所以我在前端加了时间范围筛选。用户可以选择一个年份区间,看这个区间内获奖运动员的年龄分布直方图、不同项目的身高体重散点图。

有意思的是,把几十年的运动员年龄分布叠在一起,能看到参赛年龄逐年增长的趋势,这跟运动科学化、职业生涯延长是有关系的。这个图用什么库画不重要,重要的是在分析前要先过滤掉缺失值,并且要关注样本量的变化,否则早期数据稀少会把分布形状拉偏。

5.5 奖牌榜排名动态排行:用柱状图竞赛展示历史进程

这个模块做成了一个“按年份播放”的动画柱状图,每次播放到某一年,柱状图就显示该年累计奖牌数前十的国家。从 1896 年一路播到 2016 年,非常直观地看到谁是早期霸主、谁在战后强势崛起。

这个图看着花哨,后端逻辑其实就是做了个累计求和:

cumulative = medal_event_df.groupby(["Year", "NOC"])["Medal"].count().groupby(level=1).cumsum()

然后按年份切片输出前十名。难点不在计算,而在前端动画的数据结构组织。ECharts 的timeline组件要求数据按年份切片传,我预先在后端生成好一个对象,key 是年份,value 是该年的前十名列表,前端直接循环渲染。

6. 实操中的典型问题与优化方案

6.1 中文乱码与字体问题

第一次做出来的图表,中文标签全变成了小方块。原因是 ECharts 默认字体在部分服务器环境里不包含中文字形。

解决办法有两个方向:一是前端引入中文字体文件,比如加载一个 woff 格式的开源中文字体,二是将图表渲染为图片类型时给字体配置指定一个服务器上已安装的中文字体名称。

我实际用的方案是前者,在页面头部预加载字体文件,Velocidade 会好很多。但要注意字体文件体积大,加载会拖慢首屏。折中方案是只给标题和 tooltip 使用特殊字体,正文用系统字体。

6.2 团体积分算法对排行结果的影响

前面提过,团体项目如果按运动员行数直接计数,奖牌数会虚高。但不同业务口对“团体金牌”的统计口径并不一致,有的按人算、有的按枚算。我最终实现的是在系统设置里加了一个“统计口径切换开关”,默认按枚数计,但也提供按人头计的选项。

这个开关看似简单,实际做的时候牵扯到很多地方:国家排行、趋势图、优势项目热力图,都依赖同一个统计入口。所以我把统计函数独立成一个medal_stat.py模块,内部统一调count_medal_by_event()这个函数,切换口径时只需要改一个参数。主线代码里不要到处直接对 Medal 做 groupby 计数,这是这个项目里最重要的代码组织经验。

6.3 大数据量下的响应速度优化

完整数据集行数在几十万级,对 Pandas 来说不算大,但如果在 Web 请求里实时跑groupby聚合,前端会明显卡顿,尤其是热力图需要返回几百个网格数据的时候。

我的优化方案是三层:

  • 预计算并输出 JSON:所有核心指标在系统启动时计算一次,写入内存或静态 JSON 文件,后续请求只做过滤和切片,不再跑聚合。
  • 前端数据降采样:当返回数据点超过 1000 个时,直接用最大最小值和间隔采样,牺牲一点点精度换流畅度。趋势图这类场景,时间轴的等间隔采样视觉上几乎无差别。
  • 启用 gzip 压缩:Web 层开启 gzip 后,大 JSON 的传输耗时下降非常明显。

6.4 历史数据一致性校验

清洗完后一定要做一致性校验,否则后面全白做。我用的最土但最有效的方法是“数数对照法”:

  • 对照官方渠道公布的历届金牌总数,统计数据里同届金牌总数是否一致。
  • 对照一个国家在某届的已知奖牌数,看系统计算出来的数字是否一致。
  • 对照多个公开数据源之间的数字差异,确认自己的统计口径是哪个。

如果校验发现差异,先不要急着改代码,先分组看数据。多数时候问题不是代码写错了,而是原始数据集本身就有记录缺失或重复。这种情况下要做的是记录差异原因,而不是强行“适配”。

6.5 部署时的小心机

这个项目我最终部署在一台小内存云服务器上。有一些经验值得分享:

  • 不要用开发服务器直接面对访问,前面套一层反向代理,静态资源交给代理处理。
  • 数据解析和聚合脚本用定时任务预跑,结果输出到指定目录,Web 服务只读这些结果文件。这样即使脚本写的不好也不会拖垮线上服务。
  • 对耗时计算设置超时和fallback,接口挂了可以返回上一次缓存结果,不至于整个页面白屏。

7. 后续还能怎么扩展

做完这套系统后,我自己给它规划了几个扩展方向,这里也写出来给你参考。

一是预测模型。基于历史数据,用时间序列模型预测下届奥运会各国金牌数。预测结果本身可能并不准,但把“预测趋势区间”展示成图表,和最终实际结果做对比,会是很直观的数据分析演示。

二是自然语言查询。把“2008 年金牌最多的五个国家”这类问题解析成聚合查询,然后自动匹配图表。这个方向工作量不小,但对交互体验的提升是质变的。

三是参加项目横向对比。分析新增项目、取消项目对国家奖牌格局的影响。比如某大项新增小项后,哪些国家最先拿到新项目的奖牌,这个维度可以看出一个国家的项目布局能力。

我在实际做这个项目的过程中,最大的体会是:数据分析项目最耗时间的永远不是算法,而是对数据的理解和对细节的把控。一个字段的统计口径不同,产出的结论可能完全不同。但只要把清洗、校验、模块解耦这几件事做好,后续的可视化展现反而变成了最轻松的一环。

最后再分享一个小技巧:拉数据的时候记得把每一个分析模块的结论写成文字,配图存档。这样当你要向别人介绍这个系统时,不只是“我画了个图”,而是“我发现了几条有趣的规律”,这会让整个项目完成度提升一个档次。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进