尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python历届奥运会数据可视化分析系统:从数据清洗到交互式Web大屏

Python历届奥运会数据可视化分析系统:从数据清洗到交互式Web大屏 基于Python的历届奥运会数据可视化分析系统——这个标题一看就是课程设计、毕业设计或者练手项目的常见选题。但我想先把话说在前面真正把这个系统从头到尾做下来最有价值的不是最后那几张图表而是处理数据、设计分析维度和排查问题的过程。很多同学拿到开源数据集之后第一件事就是pd.read_csv然后盲目画图结果图是出了一堆导师问你的系统到底能回答什么问题时却哑口无言。这篇博文我打算完整复盘一下我搭建这套基于Python的历届奥运会数据可视化分析系统的全过程从数据基座到可视化落地再到实测踩坑把每个关键选择背后的理由也一并讲清楚希望对正在做同类项目的人有帮助。1. 为什么选奥运会数据做可视化先把分析目标想清楚1.1 可视化系统的难点不在画图在于分析目标的定义先聊一个我在很多项目里反复看到的通病拿到数据就开画画完再想意义。数据可视化分析系统核心词有两个一个是分析一个是系统画图只是手段。奥运会数据集之所以适合做可视化练手是因为它在数据结构上有天然的多样性有年份时间维度、有国家地区地理维度、有比赛项目类别维度、有运动员个体记录人物维度、有奖牌类型指标维度。这种多维结构决定了它可以回答很多有意义的问题而不是只能输出一张某年奖牌榜的静态图。我在系统设计最开始做的事情不是写代码而是列了一堆业务问题然后反过来想哪些问题用图回答最直观历届奥运会总规模怎么变化——用参赛人数、参赛国家数、比赛项目数随年份的变化曲线来回答。哪些国家在奖牌榜上长期霸榜哪些国家在某些年代突然崛起——用多国家奖牌趋势折线来回答。奖牌榜TOP10的国家金牌、银牌、铜牌结构有什么差异——用堆叠柱状图来回答。运动员的年龄、身高、体重在不同项目上有怎样的分布差异——用箱线图、直方图来回答。哪些大项产生的奖牌数量最多——用饼图或南丁格尔玫瑰图来回答。你会发现这些问题有一个共同特点它们都要求系统具备筛选、聚合、对比的能力。所以这套系统的本质不是一个静态报告而是一个带交互的OLAP式仪表盘用户选择年份、国家、项目图表随之联动刷新。这个定位一旦清楚了后面所有的技术选型都变得顺理成章。1.2 从项目标题倒推系统边界基于Python的历届奥运会数据可视化分析系统这个标题跟好看的奥运会图表是两回事。它的关键词是系统意味着需要有数据层、分析层、展示层三层结构。我在动手前给自己定的功能清单是数据层能加载历史全部夏季奥运会数据从1896年到2016年做好清洗与格式化存成规范的分析用数据表。分析层支持按年份、国家、项目、性别等维度做聚合统计输出结构化的统计结果。展示层用Web方式呈现包含多个图表卡片和筛选器图表之间能联动。我见过很多人把这类项目做成一个Jupyter Notebook里的一堆散图那不能叫系统。还有人在Excel里做数据透视表然后截图贴到Word里那也不能叫可视化分析系统。一个合格的系统至少应该能让人操作不是让人看截图。所以我最终选择了Web方案后端用Flask提供数据接口和页面渲染前端图表用pyecharts生成的ECharts交互组件嵌入到HTML页面中。这个组合上手成本低、效果上限高、代码量可控非常适合单人完成的项目。2. 数据基座拿到CSV之后先别急着画图2.1 数据源选型与字段摸底目前网上流传最广的开源奥运数据集是那份收录了120年夏季奥运会运动员参赛记录的CSV文件名通常是athlete_events.csv。这个数据集覆盖1896年到2016年所有夏季奥运会共有27万行左右的记录每条记录包含运动员ID、姓名、性别、年龄、身高、体重、国家NOC、年份、赛季、城市、运动项目、事件小项和奖牌类型。拿到数据后的第一件事永远是摸底不是画图而是df.info()、df.describe()、df.isnull().sum()。我把字段归成了三类直接可用字段Year、Season、City、Sport、Event、Medal、Sex。需要加工字段Age存在大量缺失、Height和Weight缺失且单位不统一、NOC国家代码需要映射到完整国家名。不建议直接使用的字段Name但可以用于运动员维度分析、ID。这里特别提醒一句很多教程里直接把Height当作数值型字段用了但你用df[Height].dtype看一下会发现它实际是浮点型这没问题关键是缺失率接近20%如果不处理后面做年龄/身高分布图时会出现大量空桶。更隐蔽的问题是这个数据集里年龄是很多运动员在比赛当年的年龄而不是出生年份计算出来的所以做运动员年龄分布分析时必须明确标注这个口径。2.2 国家与地区字段归一化最容易翻车的环节这是整套系统里我花时间最多的部分也是绝大多数人会忽略的部分。原始数据里的NOC字段是三个字母的国际奥委会代码形如USA、CHN、URS它本身能直接用于聚合但问题是如果你想做地图可视化、或者想比较同一个国家在不同历史时期的表现直接用原始代码会出大问题。为什么因为国家实体在历史上发生过大量合并、分裂和更名。举几个我实际处理过的例子1952年到1988年之间的德国分为东德GDR和西德FRG1992年后统一为德国GER如果按原始NOC聚合德国数据是碎的。苏联URS在1992年以独联体EUN名义参赛1996年后拆成俄罗斯RUS等多个国家。捷克斯洛伐克TCH在1996年之后变成捷克CZE和斯洛伐克SVK。英国在数据集里的代码是GBR但中文习惯叫英国如果要做地图ECharts的地图注册名又是United Kingdom。我建立了一张国家别名映射表把每个NOC代码映射到一个统一的国家/地区标准名称上同时保留了历史实体字段用于区分。比如德国的统一名称是德国但历史实体字段标记为西德或东德。这样既保证了长期趋势分析的连续性又保留了历史细节。这一步做完后面所有的图表才有可靠的地理和国别维度可用。2.3 空值与单位杂质的全面清理这个数据集的奖牌字段有个特点没有获奖的运动员Medal字段是NaN而不是无奖牌。这意味着想做获奖人数/参赛人数占比分析时不能直接groupby得先把Medal用fillna(None)补上或者单独构造一个布尔字段has_medal。年龄字段的缺失率不算太高但身高体重的缺失率在部分项目里很离谱比如摔跤项目的早期数据几乎全是空的。我在处理时没有盲目删除行而是用了分层聚合填补的策略按Sport和Sex分组用组内中位数填充缺失的年龄和身高体重。为什么不直接用全局均值因为体操运动员的中位身高和篮球运动员的中位身高差了十万八千里全局均值会严重扭曲分布特征。这一步是很多教程不会讲的细节但对后续箱线图分析影响非常大。另外还要处理单位问题。身高体重在大部分记录里是厘米和千克但我检查过个别记录的异常值比如身高出现1860这样的数值一查发现是少数民族语言的记录把单位写成了英尺或者干脆是数据录入错误。针对这类问题我没有做复杂的单位自动识别而是用了一个简单粗暴的策略设定合理范围身高80-230cm体重30-200kg超出范围的行标记为异常并在分析时过滤掉。数据清洗的目标不是完美而是够用且不误导分析结论。清洗完成后我把结果存成了一个SQLite数据库包含athletes、medals和country_map三张表。为什么不用直接读CSV因为Web系统启动后每次页面刷新都要重新触发数据加载和清洗逻辑效率太低而且原始CSV的清洗是只做一次的事不应该重复执行。存进SQLite之后后端接口只需要写SQL查询就好了逻辑清晰不少。3. 可视化方案选型与Web系统搭建3.1 为什么我选pyecharts Flask而不是别家可视化方案的选择我其实是纠结过的。排除法如下Matplotlib/Seaborn出图质量高适合论文插图但是静态的没有交互用户没法在页面上悬停看具体数值、框选缩放不适合当系统用。Plotly/Plotly Dash交互很强但中文化和定制风格需要额外调优而且生成的文件体积偏大部署也略重。ECharts原生 JavaScript效果天花板最高但全是成都前端代码对一个以Python为主的项目来说工作量会翻倍。pyecharts它的价值在于把ECharts的配置项封装成了Python接口可以用纯Python生成一个完整的交互式图表HTML然后通过render_embed()直接嵌进Flask模板里。这意味着我写后端的时候顺便把前端图表也解决了不碰一行JS对单人项目极其友好。所以我的技术栈是FlaskWeb框架 pyecharts图表生成 SQLite数据存储 Bootstrap页面布局和筛选器样式。这个组合还有一个隐藏优势pyecharts的图表本质上是ECharts实例它默认支持鼠标悬停显示数值、图例开关、数据缩放这些交互能力是白送的不需要额外开发。3.2 核心图表的选型逻辑与关键代码我做系统的时候没有追求图表数量多而是追求每个图表能准确回答一类问题。下面是五个核心视图的设计逻辑以及对应的核心代码片段。视图一历届奥运会规模趋势折线面积图回答奥运会是怎么一步步变大的。X轴是年份Y轴可以切换为参赛国家数、参赛运动员数、比赛小项数。因为三个指标的量级差异很大我做了双Y轴左侧显示人数右侧显示国家数/小项数。代码上用Line组件加一个AreaStyle让面积渐变视觉上更有层次。from pyecharts.charts import Line from pyecharts import options as opts line ( Line(init_optsopts.InitOpts(width100%, height360px)) .add_xaxis(years.tolist()) .add_yaxis( 参赛人数, athlete_counts.tolist(), is_smoothTrue, linestyle_optsopts.LineStyleOpts(width3), label_optsopts.LabelOpts(is_showFalse), areastyle_optsopts.AreaStyleOpts(opacity0.15), ) .set_global_opts( title_optsopts.TitleOpts(title历届奥运会参赛规模变化), datazoom_opts[opts.DataZoomOpts(range_start0, range_end100)], legend_optsopts.LegendOpts(pos_top2%), ) )视图二奖牌榜TOP10堆叠柱状图回答谁是最能打的国家。这里用的是金银铜堆叠柱状图而不是普通柱状图因为堆叠之后一眼就能看出奖牌结构有些国家金牌多、铜牌少说明项目统治力强有些国家奖牌总量大但金牌占比不高。用户切换年份后这张图会动态展示该年份的TOP10榜单。视图三指定国家的奖牌长期趋势多系列折线图回答某个国家/地区在历史长河中的起落。用户通过多选下拉框选择几个国家系统画出各国历年金牌数的对比折线。这里有个重要细节早期奥运会很多国家没有完整参赛记录金牌为0是常态折线会出现大量贴地飞行的片段这是真实历史不能为了美观强行平滑掉。视图四运动员身体条件分布箱线图回答不同项目的运动员体型差异有多大。我把篮球、体操、游泳、举重、马拉松这几个差异明显的大项拿出来用Boxplot展示身高分布再配合一张年龄分布的直方图。当年我在测试时第一次看到篮球运动员身高分布和体操运动员身高分布重叠部分极小的箱线图时确实觉得这种对比是普通表格很难传达的。from pyecharts.charts import Boxplot box Boxplot() box.add_xaxis(sports.tolist()) box.add_yaxis(身高分布, box.prepare_data(height_data_by_sport)) box.set_global_opts(legend_optsopts.LegendOpts(pos_top2%))视图五金牌地理分布世界地图回答金牌在哪里产生。ECharts的世界地图需要把国家名与地图注册名对应上这是后面要讲的坑但效果确实震撼颜色深浅一眼就能看出奖牌格局。3.3 交互与布局仪表盘不是图表的堆砌很多人在Flask页面里把五张图竖着排成一列就觉得完成了这其实不算一个系统只是一个图库。我做了一个顶部的全局筛选栏包含年份滑块、国家多选、大项下拉框三个筛选组件所有图表都注册了对应的回调用户改年份所有图表数据刷新用户选国家趋势图和分布图重新聚合。这里的技术实现并不复杂Flask路由里接收筛选参数查询SQLite并重新生成图表HTML片段然后用Ajax局部刷新对应的div。但它的意义在于把被动看图变成了主动探索这正好契合分析系统这一定位。交互设计时要克制不要搞一大堆花哨的筛选器三个足够覆盖绝大多数分析场景了。4. 系统实测中踩过的四个坑及完整排查过程这个项目代码不算多但测试阶段出了不少问题其中四个是最典型的我按排查链路拆开写说不定你也会遇到。4.1 坑一pyecharts版本混乱导致图表静默空白症状是Flask页面能打开其他元素都正常但图表区域全是空白浏览器控制台没有任何报错。排查链路是这样的我先检查pyecharts生成的HTML文件是否包含完整脚本——直接在Python里执行render()生成一个独立HTML用浏览器打开发现独立文件能正常显示。那问题就出在Flask嵌入环节。我又怀疑是render_embed()的问题打印出返回的HTML片段发现ECharts的JS库被重复引入了。原来我的页面模板底部为了保险手动加了一个ECharts CDN链接而render_embed()默认会再带一份ECharts script标签两个版本冲突ECharts实例初始化失败图表就静默消失了。解决办法移除模板里的手动CDN引用完全交给pyecharts管理资源引用同时统一了pyecharts版本到1.x。这里强烈建议你在项目里用pip freeze requirements.txt锁定版本因为这个库0.x和1.x的API差异很大很多网上教程还是0.x的写法照抄必翻车。4.2 坑二世界地图全空的名称映射问题症状是地图组件渲染出来了图例和配色都正常但地图上所有区域都是灰色没有颜色深浅的变化。排查时我先确认了数据本身有值——同一份数据做柱状图没问题说明问题出在地图的区域名匹配上。ECharts世界地图的每个区域名是有标准的它要求名称必须匹配地图GeoJSON里的name字段。举个例子数据集里是USA、映射表转成中文后是美国但ECharts世界地图注册的名词是United StatesRussia对应的注册名是Russia这个没问题Great Britain对应的注册名是United KingdomSouth Korea对应的是South Korea还是Korea不同版本还不一样。pyecharts的Map组件做数据匹配是精确匹配一个名字对不上整个国家的数据就显示不出来。排查方法我写了个脚本把地图上所有区域名导出来跟映射表里的国家名做了差集比对逐个补齐别名映射最终维护了一张包含英文注册名、中文常用名和NOC代码的三向映射表。这个坑的教训是地图可视化从来不只是传数据进去那么简单名称对齐是必要的前置工作。4.3 坑三中文字体显示成方块和坐标轴标签重叠症状有两个。第一个是图表标题和坐标轴里的中文变成方块但页面其他部分中文正常。排查发现这是ECharts渲染图层使用默认字体导致的问题在InitOpts里指定font_family为系统字体可以解决但我没有这么处理而是在CSS里统一设置了font-family: Microsoft YaHei, PingFang SC, sans-serifECharts画布会继承页面字体问题直接消失。第二个是横坐标年份或项目名过长导致标签重叠。北京奥运会那届年份多了、项目名称长的时候就特别明显。解决办法是用axislabel_optsopts.LabelOpts(rotate45)把标签旋转45度再配一个interval: 0强制显示全部标签。注意如果你用datazoom做了缩放标签重叠问题在缩放后会自动缓解但要保证初始视图是正常的。4.4 坑四页面加载慢首屏要等好几秒症状是整个仪表盘第一次打开时要等很久才出数据数据库里其实只有27万行不应该这么慢。排查链路走了一遍先看是哪个环节慢我在Flask每个路由里加耗时打印发现慢的不是查询而是图表渲染前的数据聚合和多次查询重复执行。原因是我有多个图表每个图表各自写了一套查询逻辑比如奖牌榜TOP10和国家奖牌趋势其实都依赖同一份按国家年份聚合的DataFrame但我每次都重新从SQLite里取原始记录再groupby一次等于把同样的大表读了好几遍。解决办法分两步第一在Flask路由层做一次统一的聚合查询把清洗后的奖牌明细表先按需要聚合生成一个agg_medals临时结果传给所有图表组装函数复用第二把聚合好的结果加上一个简单的内存缓存同一个筛选参数的请求直接返回缓存结果。优化后首屏从3-4秒降到了1秒以内体感好了很多。5. 做完这个系统后我认为值得注意的几个实操体会项目收尾之后复盘有几件事是我如果再做一个同类系统一定会从一开始就坚持的。第一清洗逻辑一定要和展示逻辑解耦。我最初的代码是清洗完数据直接在Notebook里画图一切正常但把画图代码搬进Flask时清洗逻辑又跑了一遍中间改了字段名导致后面全报错。后来我把清洗固化成一个独立的prepare_data.py脚本输出SQLite数据库Web系统只管读库不管清洗两边互不干扰。这个架构看起来简单但对单人项目来说是节省调试时间的最大功臣。第二做可视化之前先把每张图的分析动词写出来。是对比就用柱状图或折线是分布就用箱线图或直方图是占比就用饼图或玫瑰图是地理就用地图。不要一张图里既是折线又是散点又是柱状这种炫技图除了好看很难回答具体问题。第三国家名称映射表是这类国际数据集项目的隐藏工作量。如果你做的是跨境电商数据、全球气候数据、国际赛事数据请务必提前准备一份别名映射表并且用脚本校验映射表和地图注册名的匹配率不要等到上了地图渲染才发现全空。第四交互式图表的交互本身也是有成本的。每个下拉框、每个滑块都要对应一份后端查询逻辑筛选条件越多接口组合爆炸越严重。我的建议是全局筛选器控制在三个以内或者做成一个页面只服务一类分析问题的多个独立页面比如奖牌分析页和运动员分析页分开比单页面塞大量筛选器更清晰。这个系统做下来我把1896年到2016年所有夏季奥运会的奖牌变迁、各国运动员体型特征、项目奖牌分布都摸了一遍。可视化确实很有成就感但真正让我觉得项目成立的时刻是演示时有人连续切换了年份和国家看着图表联动变化并说出一句原来这几十年奖牌格局变化这么大的时候。如果你也准备做类似的Python数据可视化分析系统希望这篇复盘能帮你少走几步弯路尤其是那几个我踩过的静默坑能绕就绕。
返回列表