Python+Pandas构建奥运会数据可视化分析系统

发布时间:2026/10/15 5:11:21
Python+Pandas构建奥运会数据可视化分析系统
第一次拿到这套历届奥运会的比赛记录是在一次课程设计的选题阶段。印象很深的不是数据量有多大而是这份数据自带的那种“叙事感”——跨越百余年的运动员、代表团、项目设置、奖牌变化全部挤在一张扁平的表格里。把这张表讲成能让人看懂的故事就是我当时定下做可视化分析系统的原因。这个项目的完整标题是“基于Python大数据的历届奥运会数据可视化分析系统”说白了做的事情就是三件把几十万条历史比赛记录洗干净用Pandas拆出背后规律再用图表把规律讲出来。它适合正在找数据分析练手项目的人、准备课程设计答辩的在校生也想看看自己在“数据处理可视化”这条技能链上到底有几斤几两的开发者。今天这篇就完整复盘一遍我的实现过程包括数据结构、代码细节、图表选型、踩过的坑你看完可以直接照着自己的节奏复刻一版。1. 项目立意与技术路线1.1 为什么拿奥运会数据练手数据分析入门最怕的不是不会写代码而是找不到一份“既有意思又不太脏”的数据。电商订单数据虽然真实但通常涉及保密爬虫抓到的数据又要处理反爬和各种字段缺失自己造数据又说服不了别人。奥运会历史数据几乎是天然的练习场时间跨度长、地域覆盖面广、人的属性和比赛结果混在一起既能做全局趋势也能深入单项细节。从难度上看这份数据的清洗复杂度适中。缺失值集中在身高、体重、奖牌三列含义各不相同处理方式天差地别。分析侧又有天然的话题参赛规模怎么变、奖牌格局怎么迁移、运动员的身体条件随项目怎么分化、女性参赛比例是怎么涨上来的。这些话题不需要业务背景就能理解做出来的图表也容易让人“看得进去”。还有一个很实在的原因这类数据有公开的教学版本字段设计规整原始记录在十万行级别单机用Pandas处理完全没压力又能体验到“数据量大到需要优化”的感觉卡在了一个非常适合学习和展示的粒度上。1.2 系统目标拆解动手之前我先把需求翻译成了五个问题后面所有代码都围绕这些问题展开参赛规模是怎么演变的——每届的运动员人数、参赛代表团数、大项数量。奖牌分布有什么规律——哪些代表团长期霸榜东道主有没有“主场加成”。运动员画像有什么特征——年龄、身高、体重在不同项目上的分布差异。性别参与度的变化——一百年前和现在相比女性参赛比例有多大变化。项目设置的演进——哪些年份一口气加了大项有没有项目逐渐消失。不用追求“全维度全可视化”把这些问题想清楚系统的主线就出来了。我给自己的定位是做一个“可交互的数据分析看板”不是堆图表的展示墙。每个分析对应一个结论每个图表都要能支撑某个问题的回答这样答辩或汇报时才能讲得清楚。1.3 技术栈与选型理由我最终用的技术栈是Pandas NumPy 处理数据pyecharts 做交互图表Streamlit 搭看板外壳。整套东西用Python一个语言串完适合个人项目和课程展示。选型时其实纠结过几轮。首先是图表库Matplotlib 虽然生态稳但默认样式偏学术做出来的图缺少“系统感”交互也得额外用 mpld3 之类的插件去补。Plotly 功能很全但配置项比 pyecharts 复杂一点而且pyecharts 基于 ECharts内置了地图、滚动缩放、渐变配色等好看又好用的能力中文社区资料也足够多。框架层我考虑过两条路Flask 前端静态页面或者 Streamlit。Flask 灵活但需要自己处理前后端交互、页面路由、请求参数工作量大很多Streamlit 是“数据脚本直接变网页”缓存机制和组件系统都够用写起来非常快。对“可视化分析系统”这种偏展示型项目Streamlit 能把精力集中在分析逻辑上而不是浪费在调前端框架上。如果你正好有 Web 基础想做得更“企业级”一点用 FastAPI 提供数据接口 Vue 写前端也没有问题只是时长会明显拉长。先接受“用最少代码跑通闭环”再在闭环基础上加东西是这类项目最顺的推进方式。2. 数据清洗与预处理2.1 先把数据长什么样看清楚拿到数据第一步永远是df.info()和df.head()不要急着写聚合。我用的公开教学数据集包含运动员的历届比赛记录每一行代表“某位运动员在某一届某个小项上的一次参赛记录”。常用字段大体如下字段含义示例ID运动员唯一编号1Name运动员姓名A BSex性别M / FAge参赛时年龄24Height身高厘米180Weight体重千克80Team代表团名称ChinaNOC国家奥委会三字母代码CHNGames届次名称2016 SummerYear年份整数2016Season季节SummerCity举办城市Rio de JaneiroSport大项BasketballEvent小项Basketball Mens BasketballMedal奖牌Gold / Silver / Bronze / NaN特别注意 Medal 这一列它是 NaN 代表“没有获得奖牌”并不是数据缺失。这一点的理解直接决定后面统计对不对我在第三节会重点展开。原始数据大概是 27 万行左右70 多万条记录的说法我也见过不同版本字段会有差异但核心结构一致。第一次运行df.isnull().sum()时Age、Height、Weight、Medal 四列都有缺失这四类的处理逻辑完全不同。2.2 三类缺失值三种处理思路很多人一遇到缺失值就“fillna 填均值”这在这份数据上属于典型的错误处理。第一类是 Medal。它缺失代表“没拿奖”不是记录不完整。处理方式是单独筛出非空的行来统计奖牌而不是补一个“无”。df.dropna(subset[Medal])就是做这件事最直接的方式。第二类是身高和体重。早期赛事的身体测量数据大量缺失如果全局用一个均值填充会把短跑运动员和举重运动员的身高体重混成同一个数后续画像分析直接失真。我的做法是先按“性别 大项”分组然后用组内身高 / 体重的中位数去填充本组缺失值。中位数比均值更稳健不容易被极端值拉偏。第三类是年龄。缺失量相对少我做的处理是保留缺失行用于其他分析但在具体画图时先删除缺失值不额外填补。分析年龄这种强分布特征时填数反而会制造假分布。补充一句垃圾进垃圾出原则数据里还有少量参赛记录中 Age 为个位数明显属于录入异常我用df df[(df[Age] 10) (df[Age] 80)]做了个基础过滤。这类边界清理不需要复杂规则关键是意识到脏数据存在。2.3 类型修复、重复处理和统一存储数据类型方面Year 是整数Medal 转为 category 可以节省内存并加快聚合。字符串列里的前后空格统一 stripTeam 列可能出现相同 team 但大小写不同的情况用str.title()或统一大写都比手工一个个查靠谱。重复值主要出现在“同一届同一小项里同一运动员可能被分配了多条记录”这种情况下。我用subset[ID, Games, Event, Medal]做去重保留第一条。不是所有重复都是错误所以这一步要结合自己对数据语义的理解来定主键。清洗完成后我建议存一份中间结果import pandas as pd df pd.read_csv(athlete_events.csv) # 类型与基础清洗 df[Year] df[Year].astype(int) df[Medal] df[Medal].astype(category) df[Team] df[Team].str.strip() df df.drop_duplicates(subset[ID, Games, Event, Medal]) # 性别大项分组填充身高体重 df[Height] df.groupby([Sex, Sport])[Height].transform(lambda x: x.fillna(x.median())) df[Weight] df.groupby([Sex, Sport])[Weight].transform(lambda x: x.fillna(x.median())) df.to_parquet(athlete_clean.parquet, indexFalse)后面所有分析直接读 parquet 而不是原始 CSV读取速度快数据类型也保留得更好。这个习惯在数据量再大几倍时收益尤其明显。3. 核心分析模块实现3.1 规模趋势线图里藏着一个世纪的膨胀先看最直观的规模问题。要回答“参赛总人数怎么变化”本质是对 Year 分组后对唯一运动员去重计数。注意不能直接对行做 count因为同一个运动员在当届可能参加多个项目会产生多条记录。athlete_count ( df.groupby([Year, Season]) .apply(lambda g: g[ID].nunique()) .reset_index(name运动员数) ) games_count df.groupby(Year).agg( 参赛代表团数(NOC, nunique), 大项数(Sport, nunique), 小项数(Event, nunique), ).reset_index()画出来的折线图有个非常明显的节奏奥运会到了上世纪后半叶曲线基本是陡峭向上的这说明奥运会的“规模膨胀”不是渐进的而是战后全球化带动参赛群体扩张的结果。同时可以看到小项数的涨幅比大项数高得多说明一个运动下拆分出的类别越来越多。做这类分析时年份轴建议强制用折线而不用柱状因为折线能更好地表现连续变化的斜率。如果年份太多可以用 pyecharts 的 dataZoom 组件拖拽缩放保留整体趋势的同时也允许查看早期细节。3.2 奖牌格局统计前先想清楚“奖牌记录”是什么奖牌统计是这份数据里最容易算错的地方。直接df[df[Medal].notna()]得到的是“每一枚奖牌对应的运动员项目记录”但“一支队伍拿一块金牌”和“一个个人项目拿一块金牌”在记录里不一样。团体项目比如足球、篮球、接力一支队伍有十几名运动员每人都会各有一条 Gold 记录如果直接对行计数团体项目的权重会被无限放大。我当时处理的方法是分两步先对 “Year NOC Event Medal” 去重再统计总数。这样一支队伍一枚团体金牌只算一次。medal_df df.dropna(subset[Medal]).copy() medal_df medal_df.drop_duplicates(subset[Year, NOC, Event, Medal]) medal_count ( medal_df.groupby([Year, NOC]) .size() .reset_index(name奖牌数) )如果要展示“奖牌榜排名”我建议给金、银、铜设置权重比如金牌3分、银牌2分、铜牌1分做加权排序。单纯按总数排会掩盖“金牌含金量”的差别用加权分会更接近大众认知中的奖牌榜顺序。另一个容易忽略的角度是东道主效应。把“举办当年的奖牌数”和“该代表团前后几届的平均奖牌数”做对比能看到明确的主场增益。这个现象放在图表里特别有话题性很适合答辩时作为分析亮点。实现上只需要先筛出举办城市对应的年份和东道主NOC再和它前后各一两届的数据合并比较。3.3 运动员画像身高体重的项目差异这个模块用来回答“不同项目的运动员到底哪里不一样”。我分别画了年龄、身高、体重的分布并且按大项分面展示。年龄分析要处理好“届次”和“个人”的关系同一个运动员可能参加多届但年龄列本身就是参赛当届的年龄所以直接以每行记录做统计是合理的。实际数据里击剑、马术等项目的老将比例明显偏高而体操项目年轻选手密集这和项目对身体素质和经验要求的差异完全对得上。身高体重的处理要用箱线图而不是折线图。箱线图能同时展示中位数、四分位距和离群点。比如篮球、排球运动员身高中位数远高于其他项目举重运动员的体重分布偏大长跑选手则集中在中低体重区间。这一步极大地体现了“数据能够讲述物理差异”这件事。3.4 性别比例从极少到接近均等的百年变化为了统计女性参赛比例先按年份分组建一个二维表gender_by_year ( df.groupby([Year, Sex])[ID] .nunique() .unstack(fill_value0) ) gender_by_year[女性占比] ( gender_by_year[F] / (gender_by_year[M] gender_by_year[F]) * 100 )结果非常显著早期女子参赛比例是个位数后来逐届攀升近年已超过 47%早期很多年份甚至没有女子项目记录。画成面积图或堆叠柱状图以后听众会非常直观地感受到“奥运会性别平等进程”的推进速度。如果你愿意再深入一层可以挑几个首次出现女子项目的年份做标注比如在对应年份点上加一条 markLine说明“这一年首次增设女子大项”。图表里带着历史背景整个项目的叙事层次立刻就不一样了。4. 可视化与交互系统落地4.1 图表选型不是所有数据都适合同一种图我对自己定了一条规矩先确定“这个分析要回答什么问题”再决定用什么图表。下面这张表是我在实践中反复调整后形成的选型对照你可以直接用分析问题推荐图表选型原因参赛人数随年份变化折线图强调时间趋势和斜率变化各国奖牌对比横向柱状图 / 地图排名对比适合条形地理分布适合地图运动员年龄分布直方图 / 箱线图展示集中趋势、离散程度各项目身高体重差异箱线图同时看中位数和离群点性别比例随时间变化堆叠面积图展示两部分结构变化项目设置演变桑基图或热力图展示项目从无到有、从少到多我在早期犯过的错误是看到什么顺眼就画什么结果一屏堆了五六个饼图信息量却不增反减。后来统一改成“先有结论后配图表”整体观感立刻干净很多。4.2 交互看板用 Streamlit 把脚本变成系统Streamlit 的优势是写起来快。我把它拆成三栏布局左侧放筛选器和参数主区域放核心图表组件底部放数据表格和原始数据预览。典型结构如下import streamlit as st import pandas as pd from pyecharts.charts import Bar, Line from pyecharts import options as opts st.set_page_config(layoutwide) st.title(历届奥运会数据分析看板) st.cache_data def load_data(): return pd.read_parquet(athlete_clean.parquet) df load_data() year_range st.sidebar.slider(年份范围, int(df[Year].min()), int(df[Year].max()), (1948, 2016)) season st.sidebar.selectbox(季节, [Summer, Winter]) filtered df[(df[Year].between(*year_range)) (df[Season] season)]用st.cache_data把数据加载缓存起来页面切换筛选条件时不会反复读取文件。这一点对体验提升非常大尤其当数据量超过十万行后每操作一次就重新读CSV的卡顿感会让人怀疑人生。图表渲染我用st_pyecharts插件把 pyecharts 图实例塞进 Streamlit代码大概是from streamlit_pyecharts import st_pyecharts bar ( Bar() .add_xaxis(years) .add_yaxis(参赛人数, counts) .set_global_opts(title_optsopts.TitleOpts(title各届参赛人数变化)) ) st_pyecharts(bar)这样一个带筛选项的交互看板就成型了。你还可以通过st.radio切换“按届次看”“按代表团看”“按项目看”三种视图每个视图对应独立函数代码结构比把所有逻辑写在一个主文件里清晰得多。4.3 中文显示与主题美化pyecharts 处理中文本身没有大坑字体渲染基本都是开箱即用。真正需要留意的是地图组件pyecharts 的地图需要配合对应的 map 数据直接用内置世界地图时国家名称的匹配规则要和数据里的代表团名称做映射否则会出现“地图上找不到这个国家”或“名字对不上”的问题。主题统一也很关键。我用了 pyecharts 自带的ThemeType.WALDEN整体视觉效果沉稳带一点渐变质感。页面上的辅助说明统一用 Markdown 写在图表下方讲清楚“这张图说明了什么”而不是让读者自己猜。对于课程设计和成果展示这种注释习惯很加分。5. 踩坑记录与排查思路5.1 团体项目奖牌被重复计算这是我犯得最早也最典型的一个错误。初次统计奖牌榜时我直接用medal_df.groupby(NOC).size()出来结果里足球、篮球强国的奖牌数高得离谱。排查后才发现团体项目里每个队员都有一条奖牌记录而不是每个项目一条。用 “Year NOC Event Medal” 四列去重后再计数数据才对得上。这个问题特别提醒一点代码跑出来没报错不代表结果正确。做数据分析一定要对最终数字有大致的量级预期比如金牌总数应该和当年小项总数接近如果统计出的金牌数是小项数的十几倍就该怀疑是不是把运动员级别记录当成了项目级别记录。5.2 缺失奖牌参与进了不该进的聚合另一个隐蔽问题是奖牌列里的 NaN 在 groupby 聚合时会被默认忽略。如果你把 Medal 直接作为 groupby 的列再配合 agg 函数很容易得到“每个组里非空奖牌数”的隐含结果而不是“这个组的总记录数”。这会导致某些统计图里部分年份的柱状图突然“缺了一块”。我的解决方案是区分两条分析链路一条基于全量记录用于参赛人数、项目数、性别比例一条只基于奖牌非空记录用于奖牌分布。两条链路的数据框在代码里分明注释好避免混淆。5.3 CSV 加载慢越到后期越明显数据量从几万行涨到几十万行后CSV 加载速度和内存占用就成了体感瓶颈。Streamlit 每次刷新都加载一遍原始 CSV 的话页面首屏要好几秒。解决方法是前面提到的read_parquet缓存转换以及st.cache_data缓存加载结果。如果以后数据量继续涨到百万行可以用dtype参数在读取时指定列类型或者转成 DuckDB 做 SQL 查询但当前这个体量没必要过度设计。5.4 地图匹配不上初始就显示空白用世界地图展示奖牌分布时常见的问题不是代码报错而是“图出来了但全空白”。这里要注意两点一是 pyecharts 的地图包要单独安装比如全球地图数据二是 NOC 三字母代码需要先映射成地图组件认识的国家名称。如果数据里是国家代码而不是国家全名必须做一张NOC 到全名的映射表否则地图数据根本对应不上。我当时对照数据里的代表团列表手工整理了一批映射并把它写成独立的 JSON 文件方便后续复用。5.5 图表文字重叠与坐标轴截断在年份跨度较长、柱状条较多的图里默认配置下 x 轴标签会挤成一团。最省事的办法就是开启 dataZoom让用户自己缩放查看区间另一种是旋转标签或设置interval控制标签密度。当然更好的做法是给坐标轴设置一个合理的起始范围避免自动缩放的默认区间把图拉得变形。6. 可扩展方向与个人心得6.1 加一点预测系统会更完整如果做完了基础可视化还想再更进一步可以尝试“奖牌数预测”模块。思路是先按“Year NOC Sport”聚合历史奖牌、参赛人数、上届奖牌数、是否东道主等特征再用简单的回归或随机森林去预测下届奖牌数。不少项目用这套思路拿到了不错的演示效果它把项目从“纯可视化”升级成了“分析和建模结合”答辩时说服力更强。6.2 代码组织的几个实用习惯我这次最大的教训是不要写“一个脚本跑到底”。规范化做法是把代码按功能拆分data_preprocess.py负责清洗和存储、analysis_modules.py放统计函数、dashboard.py只做展示。函数命名也要直白比如plot_gender_ratio(filtered_df)比plot1()可读性高得多。再配合 Git 管理不同版本改坏了随时可以回滚这在试错型项目中特别有用。还有一个小技巧把每次分析产出的关键结果保存成 CSV 或 JSON比如“每届每 NOC 的奖牌数”是会被反复使用的中间数据。提前缓存中间结果看板交互速度会有非常直观的提升。6.3 复盘的一点体会从拿到这份数据的兴奋到中间被团体项目奖牌数翻车浇了一盆冷水再到最终把看板跑起来整个过程的收获其实不止是“会了 Pands 和 pyecharts”。更难得的体验是完整走了一遍“提出问题 — 找数据 — 清洗 — 分析 — 可视化 — 验证结论”的闭环。这种闭环在真实工作里正是最常用的思考方式先想清楚要解决什么问题再考虑用什么手段而不是先迷恋某个工具。如果给你一个启发我希望是这句话别急着把所有图表堆满先把数据读懂。读懂之后故事自然就有了。