Python数据可视化实战:基于Plotly与Flask的奥运会数据分析系统

发布时间:2026/9/7 16:43:40
Python数据可视化实战:基于Plotly与Flask的奥运会数据分析系统 最近把一个拖了挺久的项目收尾了就是标题里这个“基于Python的历届奥运会数据可视化分析系统”。这名字看着有点长其实拆开就三件事用Python拿数据、清洗数据、再把数据变成能看懂的图表。从零开始搭到最终能跑通中间踩了不少坑也攒了不少经验今天把这套东西从头到尾捋一遍当作一次完整的项目复盘。这个系统适合谁参考如果你是做数据分析或者Python相关课程设计的学生或者想入门数据可视化、又不想用现成BI工具、想自己用代码控制每个细节的人那这篇文章会很对路。我尽量把思路、代码、踩坑记录都写出来你可以直接照着思路去复现也可以按自己的需求改。先说结论这套系统最终实现了从Kaggle公开数据集自动读取、清洗、聚合再通过Plotly生成十几类交互图表最后用Flask搭了一个本地Web页面把奖牌榜、参赛趋势、运动员特征分析全部串在一起。整个过程用到的核心库只有pandas、plotly、flask这几个对新手来说非常友好。1. 项目起底与设计思路1.1 为什么要做这个可视化系统奥运会数据是一个很有意思的分析素材它横跨了时间、国家、项目、运动员个人特征等多个维度从1896年第一届现代奥运会到2016年里约奥运会一百二十年的数据里藏着大量可以挖掘的信息。比如各个国家的奖牌数变化、性别比例的演进、运动员年龄和成绩的关系、不同项目对身体条件的要求差异这些都是比较热门的话题点。我最初想得很简单用Excel做几张图不就行了但真的动手之后发现在Excel里处理27万条记录、十几个字段的数据透视表虽然能扛住但要做成联动、交互、可按年份筛选的图表操作成本太高了。而且Excel图表的样式和交互能力比较有限想做个能拖拽、悬浮提示、按条件下钻的效果基本做不到。所以最终还是选择用Python写一套完整的分析系统。另一个动力是用代码实现整个流程本身就是一个很好的练手项目。从数据获取、清洗、聚合到用Plotly画图、用Flask搭页面一条链路走下来pandas的绝大多数常用操作都能覆盖到。我自己是在做这个项目的过程中才真正把groupby、merge、pivot_table这些操作吃透的比光看文档有效得多。1.2 技术栈选型与理由技术选型这件事我一开始犹豫过几个方案最后定下来的是pandas plotly flask中途也试过ECharts还专门写过两版原生HTMLECharts的页面。先说说为什么用pandas而不是直接用SQL处理。这个数据集是CSV格式的虽然也可以导入到SQLite或者MySQL里查但27万条数据不算大pandas加载到内存里也就几百MB处理起来非常快。而且pandas的链式操作语法在处理这种结构化数据时非常顺手一个groupby加agg就能完成多维度的聚合统计比写一长串SQL更直观尤其适合在Jupyter里边跑边看结果。可视化部分我有过一段纠结。ECharts的图表确实好看交互也流畅但它的数据格式要求比较死板需要把数据规整成固定的JSON结构再喂给前端。而Plotly是直接用Python调用的API设计更符合数据分析的习惯DataFrame可以直接传给Plotly画图不用手动转格式。另外Plotly默认就带缩放、悬停、框选这些交互功能对快速搭建分析系统来说省了很多事。Web框架我选了Flask理由很简单轻量不需要学Django那一套复杂的东西。这个系统的核心是数据分析Web端只是一个展示壳子Flask一个路由、一个模板就能把图表传过去够用且容易理解。1.3 系统整体架构整个系统分四层数据层、处理层、分析层、展示层。数据层就是那个CSV文件7万多行不对实际是27万行15个字段包含从1896到2016年每位参赛运动员的基本信息和获奖情况。处理层负责数据清洗包括缺失值处理、字段类型修正、派生字段的构建。分析层是我写的各种统计函数按国家、年届、运动项目、运动员个人特征等维度做聚合。展示层是Flask页面把分析层产出的图表和表格渲染成可交互的Web页面。这里要说一下我对“分析系统”这个词的理解。很多人以为分析系统一定要有机器学习模型其实不是。分析系统的核心是“多维度的数据汇总与对比”饼图柱图折线图这些基础图表用好了一样能产出有价值的信息。比如通过折线图看每届奥运会的参赛人数变化一眼就能看出三次世界大战对奥运会的影响这种发现不需要任何高级算法纯粹是数据可视化带来的直观洞察。2. 数据获取与预处理实录2.1 数据集来源和字段拆解数据用的是Kaggle上的“120 Years of Olympic History: Athletes and Results”数据集这是一个比较经典的公开数据集。里面有两个文件我实际用到的是主文件athlete_events.csv包含271116条记录每条记录对应一位运动员在一次奥运会中的一个参赛项目。15个字段分别是ID、Name、Sex、Age、Height、Weight、Team、NOC、Games、Year、Season、City、Sport、Event、Medal。我做了个表格方便后面看清洗逻辑字段名类型含义主要问题IDint运动员唯一编号无Namestr运动员姓名大小写混杂Sexstr性别M/F无Agefloat年龄岁大量缺失Heightfloat身高厘米大量缺失Weightfloat体重公斤大量缺失Teamstr国家/地区代表队随历史变更NOCstr国家奥委会三字代码无Gamesstr届次如2016 Summer需拆分Yearint年份无Seasonstr夏季/冬季无Citystr举办城市无Sportstr运动项目大小写混杂Eventstr具体小项含性别前缀Medalstr/NaN奖牌类型NaN语义需明确有一个容易忽略的细节就是Medal字段里的NaN并不是“数据缺失”而是代表“未获得奖牌”。如果按普通缺失值处理直接dropna掉那数据就全没了因为多数参赛运动员没有拿奖牌。这一点在清洗时一定要处理对。2.2 清洗过程中的关键决策数据清洗这块我栽过好几次跟头。第一次做的时候我写了一长串dropna把Age、Height、Weight有缺失的行全删了结果代码跑完排查发现剩下一半都不到的数据获奖记录也被误删了不少整个人都懵了。后来才意识到这三个字段的缺失是有原因的早期奥运会没有系统的测量记录尤其是女性运动员身高体重缺失率非常高一删就把早期数据全删没了。最终我的处理策略是Age的缺失值用中位数填充Height和Weight的缺失值用同一项目、同一性别的中位数来填充而不是全数据集一起填。因为不同项目对运动员身材的要求差异很大体操运动员和篮球运动员的“平均身高”完全没有可比性用全局中位数填出来会给后续分析带来严重偏差。还有一个关于年份的坑。数据集中有1960年之前的许多记录Games字段包含“1906 Summer”这样的值如果不做处理直接用文本排序年份顺序会乱。所以我单独提取了Year字段并把类型转成int。另外1906年那一届是奥运会历史上的特殊“届间奥运会”数据集里也有记录但需要注意它对趋势分析的影响。2.3 数据校验和聚合逻辑清洗完之后我先做了一次数据校验统计一下每个国家每年的参赛人数抽查了美国、中国、俄罗斯这几个代表队的数字是否合理。这个步骤非常有必要能尽早发现“是不是哪一步把数据搞坏了”。聚合逻辑上这个系统设计了三个核心维度年届维度、国家/地区维度、运动员个人维度。年届维度就是按Year分组的参赛人数走势、项目数量变化等。国家维度就是各国的奖牌数汇总这里有个小坑是“Team”字段在不同历史时期有过合并和拆分比如苏联、独联体、俄罗斯在不同的届次有不同的Team值如果要分析国别奖牌变化最稳妥的做法是用NOC代码而不是Team名称。运动员维度用来分析年龄分布、身高体重与成绩的关系。3. 可视化方案设计与核心指标3.1 从“能看”到“能发现问题”的设计思路很多人做可视化容易陷入一个误区就是图表做得特别花哨但看完之后什么都不记得。我在设计这套系统的图表时遵循了一个原则每张图都要回答一个问题。比如第一张图历届参赛运动员总数折线图回答的问题是“奥运会规模是如何扩大的”第二张图历届奖牌TOP国家排名柱状图回答的问题是“哪个国家在奥运史上统治力最强”第三张图男子和女子参赛比例随时间变化的堆叠面积图回答的问题是“奥运会性别平等是如何演进的”。每一张图放在页面上都有明确的存在意义而不是为了凑数。我也刻意保持图表类型的多样性。整体趋势用折线图对比排名用柱状图地理分布用世界地图相关关系用散点图和气泡图构成关系用饼图或堆叠图。数据量大时用箱线图看分布。没有一种图表能包打天下只有根据数据的性质选择合适的图形信息传达效率才最高。3.2 核心图表与指标定义这套系统最终产出了六组核心图表我挑几个重点说一下指标设计。奖牌榜统计我用的指标不是简单的奖牌总数而是区分了金、银、铜的加权分数。金牌记3分银牌记2分铜牌记1分用得分来排序。只数奖牌总数的话拿10块铜牌的国家会排在拿3块金牌的国家前面显然不太合理。男女参赛比例这个指标我用了“参赛总人数中女性占比”这个单一数值画成随时间变化的曲线。1896年第一届奥运会女性参赛人数是零到2016年占比已经接近45%这个指标值的变化本身就能讲述一个很长的故事。运动员年龄分析这块我用了箱线图按项目分面看不同项目的年龄中位数和离散程度。体操运动员的年龄中位数显著低于射击和马术这个差异通过箱线图一目了然。3.3 地图可视化与颜色映射国别维度的奖牌分布我用了Plotly的Choropleth地图也就是用颜色深浅来映射数值大小。这里有个容易踩的坑就是国家名称的匹配问题。数据集的NOC字段是国际奥委会三字代码而Plotly地图的locations参数支持的是ISO-3166的两位或三位代码二者有部分不重叠。我自己的处理方法是手动建了一个映射字典把NOC代码和Plotly的ISO代码对应起来没有对应关系的国家只显示在地图上但颜色是灰色。你如果复现这个项目建这个映射表是最耗时的一步我花了大概一个多小时对着维基百科的国家代码列表一个个核对。后来想想其实可以直接用NOC和GB2260中国行政区划代码做匹配但Plotly认的是国际标准没办法只能手动来。4. 核心代码实现与效果展示4.1 数据清洗部分的代码先贴一段数据加载和清洗的代码这部分是整个项目的地基如果这里处理不对后面的图表全都会出问题。import pandas as pd import numpy as np # 加载原始数据 df pd.read_csv(data/athlete_events.csv) # 创建年份列并转为整数 df[Year] df[Year].astype(int) # 奖牌字段NaN代表未获奖填充为None df[Medal] df[Medal].fillna(None) # 划分夏季/冬季奥运会 df_summer df[df[Season] Summer].copy() # 年龄缺失值用中位数填充 age_median df_summer[Age].median() df_summer[Age] df_summer[Age].fillna(age_median) # 身高体重缺失值用同项目同性别中位数填充 # 先按项目性别分组转换后再回填 df_summer[Height] df_summer.groupby([Sport, Sex])[Height].transform(lambda x: x.fillna(x.median())) df_summer[Weight] df_summer.groupby([Sport, Sex])[Weight].transform(lambda x: x.fillna(x.median()))这段代码有几个细节值得说明。transform函数在按Sport和Sex分组后返回的是和原DataFrame等长的序列这样可以直接赋值回去不会出现索引错位的情况。lambda里调用fillna(x.median())是对组内中位数填充如果某个小组全是缺失值groupby时这个小组成员会保持NaN暂时不影响后续分析但如果后面计算严格依赖身高体重的模型需要二次处理。4.2 可视化核心代码奖牌榜和参赛趋势是最核心的两张图代码我写成了独立函数方便后续在Flask里调用。import plotly.express as px import plotly.graph_objects as go def top_country_medal_chart(df, top_n15): # 只保留获奖记录 medal_df df[df[Medal] ! None] # 按NOC分组统计奖牌数 counts medal_df.groupby(NOC)[Medal].value_counts().unstack().fillna(0) counts[total] counts.sum(axis1) counts counts.sort_values(total, ascendingFalse).head(top_n) counts counts.reset_index() # 横向条形图 fig go.Figure() fig.add_trace(go.Bar(ycounts[NOC], xcounts[Gold], name金牌, orientationh)) fig.add_trace(go.Bar(ycounts[NOC], xcounts[Silver], name银牌, orientationh)) fig.add_trace(go.Bar(ycounts[NOC], xcounts[Bronze], name铜牌, orientationh)) fig.update_layout(barmodestack, title历届奥运会奖牌榜TOP15) return fig def participant_trend_chart(df): # 每年的参赛人数 yearly df.groupby(Year).size().reset_index(namecount) fig px.line(yearly, xYear, ycount, title历届奥运会参赛人数趋势) return fig这里提一个Plotly的使用小技巧如果你希望图表是堆叠条形图务必在update_layout里设置barmodestack缺了这一步三条柱状图会叠在一个中心点上效果惨不忍睹。另外orientationh会把条形图变成横向便于显示国名因为国名通常比较长纵向显示会折行或者截断。4.3 Flask整合与页面渲染后端部分用Flask把图表对象直接传给模板利用Plotly的to_html方法把图表转成HTML片段然后嵌入到页面里。from flask import Flask, render_template import pandas as pd app Flask(__name__) # 全局加载和清洗数据避免每次请求都重新处理 df load_and_clean_data() app.route(/) def index(): chart1 top_country_medal_chart(df) chart2 participant_trend_chart(df) chart3 gender_ratio_chart(df) chart4 athlete_age_distribution(df) graphs [ chart1.to_html(full_htmlFalse), chart2.to_html(full_htmlFalse), chart3.to_html(full_htmlFalse), chart4.to_html(full_htmlFalse) ] return render_template(index.html, graphsgraphs) if __name__ __main__: app.run(debugTrue)模板index.html里的主体内容其实非常少只需要一个循环把graphs列表里的HTML片段依次输出。每个图表片段都是Plotly生成的完整div加script浏览器加载时会直接渲染成交互图表。有个性能方面的经验分享Plotly生成的HTML片段体积不小如果图表太多页面加载会变慢。我当时生成了12张图首页加载大约花了5-6秒。后来改了策略把其中的静态图表比如柱状图、箱线图改成只在点击对应导航时才按需加载而不是全部塞进首页。如果你不需要Web展示其实也可以在Jupyter Notebook里跑完所有图表效果一样还省去Web那一步的复杂度。5. 开发过程中踩过的坑与排查技巧5.1 奖牌字段的NaN被误删这是我犯过最严重的错误几乎导致后续所有分析全部作废。前面提到了Medal字段的NaN不代表缺失值而是“没有获奖”。我第一次清洗数据时习惯性地对所有字段做了dropna结果27万条数据删到只剩3万条而所有“没获奖”的运动员记录全部消失参赛人数趋势图完全失真。排查过程也很有意思。我第一眼看到趋势图时发现1920年之前的数据缺失严重第一反应是“数据源本身不完整”甚至打算去换数据集。后来随手groupby了一下Year看看每年记录数发现1936年只有不到5000条和常识对不上才回头检查数据清洗逻辑。所以这里给大家一个忠告清洗数据时先搞清楚每个字段缺失值的语义是“真缺失”还是“无记录”还是“有特殊含义”再决定怎么处理。5.2 中文显示乱码和字体问题Plotly生成的图表默认字体可能不支持中文导致图表的标题、坐标轴标签在浏览器里变成方块。这个问题我在Linux服务器上跑的时候特别明显Windows本地反而不容易触发。解决办法是给Plotly显式指定一个支持中文的字体。路径要看系统里装了哪些中文字体。Windows一般用Microsoft YaHeiLinux服务器可以安装wqy-microhei然后像下面这样配置import plotly.graph_objects as go fig.update_layout(fontdict(familyMicrosoft YaHei, WenQuanYi Micro Hei, SimHei, size14))这样设置之后中文字体优先级依次是从雅黑到文泉驿微米黑到黑体哪个可用就用哪个起码保证不是方块。5.3 国家名称映射问题这个坑我在前面提到过就是NOC代码和Plotly地图ISO代码不一致。除了手动建映射表还有一个更取巧的办法直接用Plotly的px.choropleth的locationmodeUSA-states是不行的那是美国州地图的模式。要真正解决还是得用国际标准化组织的代码或者用country names模式让Plotly用国家全名去匹配但这时你需要把NOC代码翻译成英文国家全名工作量也不小。我的最终方案是热力地图只做一个静态图把NOC代码映射到经纬度用散点地图的方式展示奖牌数散点大小和颜色都映射到奖牌总分。这样既避免了Choropleth的代码匹配问题又能直观展示全球分布。散点图还有一个额外好处就是多个国家挤在同一个区域时不会像Choropleth那样颜色被覆盖遮挡。5.4 性能优化与数据量控制初期用Plotly画散点图时数据量一大页面就直接卡死尤其是绘制所有运动员的身高体重散点图时27000多个点同时渲染页面滚动都是掉帧的。解决思路有两个方向。一是对数据做抽样或者聚合散点图抽样只选十分之一的数据点当观众需要细看时再用框选放大。二是改用密度热力图不再逐一绘制每个点而是用二维直方图统计每个格子里的数据量再用热力色阶展示密度。两种方案实测下来密度热力图渲染速度更快而且能更清晰地看到数据的集中区域反而是更好的可视化方案。另外还有一个隐藏很深的问题Flask debug模式开启时每次保存代码都会自动重新加载数据27万行数据的CSV每次要读好几秒。我查了很久才发现是debug自动reloader在作怪。解决办法是把数据加载逻辑放到一个单独的缓存模块里用全局变量缓存处理结果只在第一次请求时加载。5.5 问题排查速查表顺手整理一个速查表后面有人做类似项目可以直接对号入座问题现象可能原因解决方案图表数据明显偏少清洗时误删了Medal为None的记录先确认NaN语义再决定填充或删除中文标题显示方块系统缺少中文字体且未指定font给Plotly指定字体如SimHei或WenQuanYi地图上很多国家是灰色NOC代码和Plotly ISO代码不匹配手动建映射表或用散点地图页面加载极慢Plotly图表数量太多或数据点过多按需加载图表、采样或用密度热力图Flask每次保存自动刷新debug模式触发reloader关掉debug或缓存数据加载结果这个表格值得收藏后面做类似项目很多问题都是上面这几种的变体。6. 延伸思考与个人体会6.1 这版系统还能怎么扩展做完整套系统之后我心里其实攒了一堆后续想做的内容。最想加的是时间轴动态图也就是用Plotly的Frame动画功能做一张“历届奖牌榜动态变化”的图让各个国家在奖牌榜上的位置随年份增长而移动变化这个视觉效果比静态图冲击力强得多而且能看出不同国家的优势项目崛起时间点。另一个方向是加入预测模型。虽然奥运会受各种不可控因素影响但如果只做“下一届金牌数区间预测”这种粗粒度估计用简单的时间序列模型比如Prophet还是能跑出结果的。当年我在课程设计答辩时同组的一个同学就做了这个扩展评委老师明显对预测部分更感兴趣。如果你有时间我建议把目光放在“运动员画像”上结合身高、体重、年龄、项目这些特征用聚类算法看看能不能自动分出“短跑型”“长跑型”“力量型”这几类运动员风格然后分析每类风格在不同年代的变化趋势。这个方向既保持了体育主题又给整个系统增加了机器学习的维度能显著提升项目上限。6.2 关于数据分析的一点个人心得做这个项目最大的收获倒不是学会了某几个库的用法而是建立了一条“从数据到结论”的完整思维链路拿到数据先想清楚要回答什么问题再决定画什么图最后根据图表反馈迭代自己的假设。比如我一开始假设“发达国家金牌多是因为参赛人数多”后来把参赛人数和奖牌数的关系画成散点图发现相关性的确存在但中国是个明显的异常点参赛人数排不进前十金牌数却能稳居前三。这个发现反过来促使我添加了一个“人均奖牌效率”指标用来衡量各国运动员的夺牌效率。这个从“发现异常”到“修正指标”的过程其实就是数据分析最有价值的部分。我也越来越体会到数据可视化并不只是“把数据变成图”这个动作更重要的是“选择看什么图”这个决策。同一个数据集画多少张图、每张图用什么变量、颜色怎么映射都会影响看图的人得出什么结论。做可视化系统本质上是在搭建一个让数据说话的表达框架。6.3 给后续开发者的建议如果你准备自己动手做一个类似的数据分析系统我建议按“三步走”来推进不要一上来就写代码。第一步把业务目标写清楚。至少列出三个你想回答的问题比如“哪一年是奥运会规模扩张的转折点”“中国在哪些项目上有绝对优势”“运动员年龄分布在哪些项目上差异最大”问题清单越具体后面的分析方向越明确。第二步先探索后建系统。在Jupyter Notebook里完成全部数据清洗和图表探索确认每一张图都符合你的预期再开始搭Web框架。不要先把Flask环境搭好再来做图表否则前端后端来回调效率低到怀疑人生。第三步关注所有图表的联动和布局。如果你要做Web展示图表的尺寸、间距、缩放比例都要统一否则页面会很杂乱。我在第一版里图表尺寸不一看起来特别业余后来统一了宽度、高度和边距参数整体观感立刻提升了一个档次。这个细节虽然很小但对最终效果的呈现影响非常大。做项目就像跑马拉松前期数据清洗和方案设计花的时间往往比后期写代码多得多但这些投入最后都会在项目质量和你的理解深度上体现出来。希望这篇文章能帮你避开我走过的弯路把更多精力花在真正有价值的分析上。