这期是 Python 系列的第五篇主题落在 ECharts 数据可视化上。先用一句经验总结如果你想把 Python 处理完的数据变成一份可以交出去、能缩放、能悬停、能导出的交互图表ECharts 是我目前用过的最省事的方案。这里的“省事”不是说配置项少而是它整套交互能力是天生带着的你不需要像折腾某些静态绘图库那样为了一个悬停提示写半天辅助代码。我最早接触这个组合是在给某业务团队做月度运营报表的时候。当时对方提了个要求图表内容要能鼠标悬停显示具体数值最好还能放大看某个时间段。我一开始用老思路去画静态图画完发现根本没法交互最后只能额外生成一堆标注贴上去效果很勉强。后来换成本文要讲的这条路线同样的数据几分钟就输出一份 HTML 图表双击就能在浏览器里看悬停、框选、缩放全部默认可用。今天就把这个过程完整拆开从环境准备到图表代码再到常见报错一次性讲清楚。1. 为什么我会从 matplotlib 转到 ECharts以及 pyecharts 在中间扮演的角色1.1 一张会交互的图表省掉的不只是口舌如果你经常给非技术同事交付数据你应该能理解“静态图片”的尴尬图上数值看不清鼠标点不动想对比某个区间只能靠截图然后手动圈出来。你说“数据没问题”对方说“哦”但心里还是想自己去点一点、拉一拉。与其反复解释不如直接交付一张能交互的网页图表。ECharts 就是一个前端 JavaScript 图表库它最核心的特点是配置即图表——你用一套 JSON 结构的配置项描述图表类型、数据、坐标轴、提示框、图例它就在浏览器里渲染出可交互的图形。点开任意一张 ECharts 图表你会注意到它有自带的 toolbox、鼠标悬浮提示、数据区域缩放这对做数据分析交付来说太实用了。但问题来了我们写的是 Python不是 JavaScript。总不能在数据分析流程里为了画个图再去维护一套前端工程。于是 pyecharts 这个库出现了它把 Python 对象转译成 ECharts 需要的配置直接生成 HTML 文件或嵌入网页的片段。换句话说pyecharts 就是那座让 Python 数据流进 ECharts 画布的桥。1.2 pyecharts 的桥接原理其实很朴素pyecharts 的工作流程大体是这样的你在 Python 里创建一个图表对象例如Line()然后调用add_xaxis()、add_yaxis()把数据填进去调用set_global_opts()、set_series_opts()配置标题、提示框、图例、数据缩放等行为调用render()方法pyecharts 把这些 Python 参数序列化成 JSON并组装进一个带了 ECharts 脚本的 HTML 页面你得到一个独立的 HTML 文件别人不需要安装 Python不用启动服务双击就能打开。这套逻辑看起来很朴素实际用起来却非常契合数据分析场景。因为数据分析和报表交付本来就是“后端处理数据 前端展示结果”的分工pyecharts 帮你把前端那一步打包成了几行 Python 调用后端工程师不需要去学习完整的前端构建链路。有一次我在交付时对方办公室的电脑没有 Python 环境我就把一个 HTML 文件发过去他用浏览器直接打开拖拽缩放、下载图表图片都正常。那一刻我彻底确认了这条路的价值图表一旦变成 HTML 资产它的传递成本几乎为零。1.3 和 matplotlib、plotly 放在一起比什么时候选谁如果你只画论文插图、需要导出 PDF 矢量图那 matplotlib 依然是稳妥选择它跟科研出版体系的配合很深。但如果你要的是交互、浏览器展示、快速交付pyecharts 的交互能力和渲染体积通常更有优势。表格参考如下维度matplotlibplotlypyecharts/ECharts上手曲线较低但交互功能配置繁琐中等封装度高中等配置项需查文档交付形态静态图片为主HTML/服务端渲染HTML/页面嵌入交互能力弱需额外开发强强缩放悬停默认可用包体积与渲染性能轻较重轻量大数据量下有采样策略中文文档与社区示例多一般中文资料丰富我的习惯是如果是学术论文或打印报告走 matplotlib如果是内部看板、运营周报、临时数据分析页面优先 pyecharts。两者并不互斥很多时候我会先用 matplotlib 做快速探索确认数据形态和分析维度再用 pyecharts 做最终交付。2. 环境准备装对版本避开旧教程的坑2.1 一条 pip 命令安装但版本坑要注意安装 pyecharts 非常简单常规操作pip install pyecharts如果要升级已有环境pip install pyecharts -U但我必须提醒一句网上很多教程是旧版 pyecharts0.5.x时期写的API 和现在完全不一样。如果你搜到一个教程里面的导入写的是from pyecharts import Bar那基本可以判断它至少是几年前的内容。新版 pyecharts1.0 以后的导入结构是from pyecharts.charts import Bar from pyecharts import options as opts这个差异曾经害我多花了半小时排查。当时我在某个虚拟环境里pip instll pyecharts之后复制了一段旧代码结果一直报导入错误看报错信息又看不明白。后来用下面的命令检查版本才反应过来pip show pyecharts所以第一课先把版本查清楚再去套用教程。个人建议直接以官方文档为准或者认准新版导入写法其它随手拿来的片段都要多留个心眼。2.2 在虚拟环境里安装时最容易忽视的细节如果你用 conda 或 venv 管理 Python 环境需要注意 pyecharts 装到了哪个环境里。很多人是在全局环境装好了进入项目虚拟环境后却调不到报出ModuleNotFoundError。这种问题不属于 pyecharts属于环境管理但实际遇到时特别容易让人怀疑是库本身的问题。我的排查顺序是三步which python确认当前解释器路径pip show pyecharts确认库所在环境python -c import pyecharts; print(pyecharts.__version__)确认能正常导入并看到版本号。三步都过一遍环境问题基本就暴露了。2.3 Jupyter Notebook 的使用方式和平常脚本不太一样如果你在 Jupyter Notebook 里写代码想直接在单元格下方看到图表要调用render_notebook()而不是render()。例如from pyecharts.charts import Line line Line() line.add_xaxis([A, B, C]) line.add_yaxis(数量, [1, 2, 3]) line.render_notebook()在 Notebook 里用render()的话只会生成文件单元格里什么都看不见。第一次用的时候我差点以为代码写错了后来才意识到只是渲染目标不同。还有一个细节Notebook 里反复执行多个图表时如果发现某个图不刷新优先检查是不是单元格输出被缓存了重启内核再跑一遍往往就好了。3. 从第一个折线图开始拆解一条数据管线3.1 一个最小折线图的完整注释先看一段完整的折线图代码这是最常用的入门案例from pyecharts.charts import Line from pyecharts import options as opts x_axis [1月, 2月, 3月, 4月] y_axis [102, 135, 98, 187] line ( Line() .add_xaxis(x_axis) .add_yaxis( 销售额(万元), y_axis, is_smoothTrue, symbol_size8, ) .set_global_opts( title_optsopts.TitleOpts(title月度销售额走势), tooltip_optsopts.TooltipOpts(triggeraxis), ) ) line.render(sales_line.html)运行之后目录下会出现一个sales_line.html文件双击打开就能看到折线图。鼠标悬停在数据点上会显示日期和数值右上角还有还原、缩放等工具按钮。这段代码虽然短但已经覆盖了 pyecharts 的核心操作链Line()创建图表对象这里的类名决定图表类型add_xaxis()设置横轴数据横轴一般是类别或时间add_yaxis()添加一个数据系列系列名会出现在图例里链式调用是因为这些方法都会返回图表对象本身所以能一直点下去set_global_opts()是全局配置入口包括标题、工具箱、提示框、坐标轴、数据缩放render()输出文件最常见的后缀是.html。3.2 为什么需要opts这一层很多初学者会困惑为什么不能像 matplotlib 一样直接传参比如titlexxx这样多简单。答案在于 ECharts 本身用结构化的配置对象描述图表opts.TitleOpts、opts.TooltipOpts就是这些结构在 Python 里的投影。你想想一个标题对象不止有文字还有位置、颜色、字体大小、副标题、链接用散乱的函数参数表达很容易乱用一个配置对象表达更清晰。pyecharts 的options模块几乎对应了 ECharts 的全部配置项需要什么就查什么。刚上手时不需要背知道入口在哪就够了全局类配置找set_global_opts系列级配置找set_series_opts。3.3 折线图的数据怎么接从列表到 DataFrame实际项目中数据很少手动写在代码里通常是 pandas 读出来的。一个常见的节奏是import pandas as pd from pyecharts.charts import Line from pyecharts import options as opts df pd.read_csv(sales.csv) df[月份] pd.to_datetime(df[月份]) df[月份文本] df[月份].dt.strftime(%Y-%m) line ( Line() .add_xaxis(df[月份文本].tolist()) .add_yaxis(销售额, df[销售额].tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title销售额趋势), datazoom_opts[opts.DataZoomOpts(type_slider)], ) ) line.render(sales_trend.html)这里有个容易踩的小坑pyecharts 的add_xaxis和add_yaxis接收的是普通列表不能直接把 pandas 的 Series 丢进去。虽然某些低版本可能兼容但新版大多会断言类型或直接渲染异常。最稳妥的做法是调用.tolist()转换。另一个坑如果数据里有NaN或缺失值图表上可能出现断裂或不显示我的习惯是在传给图表之前先做一次数据清洗。3.4 渲染的其它选项SVG 和图片render()默认生成 HTML但其实也可以指定别的形式。如果初始化时指定 SVG 渲染器可以输出矢量图line_svg Line(init_optsopts.InitOpts(renderersvg))renderersvg适合对清晰度要求较高的场景比如要打印出来贴在看板上。如果你非要 PNG 图片相对麻烦pyecharts 本身不直接支持一键出 PNG需要额外借助无头浏览器截图。我实测下来的建议是与其折腾图片导出不如直接把 HTML 交付或让对方自己在工具箱里点“保存图片”浏览器生成的 PNG 质量也不差。4. 柱状图、饼图、散点图不同数据用什么视觉语言4.1 柱状图比大小、比差异最简单也最直观柱状图的语法和折线图非常相似只是类名换成Barfrom pyecharts.charts import Bar from pyecharts import options as opts categories [电子, 家电, 服饰, 食品, 美妆] values [632, 420, 518, 290, 364] bar ( Bar() .add_xaxis(categories) .add_yaxis(销售额(万元), values) .set_global_opts( title_optsopts.TitleOpts(title各品类销售额), yaxis_optsopts.AxisOpts(name万元), ) ) bar.render(category_bar.html)柱状图最适合做各类别之间的对比一眼能看出谁高谁低。如果分类很多比如十几个品牌可以考虑用reversal_axis()调转成横向柱状图这样标签文字就有足够的空间排列不会互相挤压。代码只需要在render前加一句bar.reversal_axis()横向柱状图在排名场景里特别好用比如前二十名商品排行竖着的柱子会挤成一团横过来就清楚很多。柱状图还有一个堆叠模式适合观察总量构成。比如三个区域各自的销量bar.add_yaxis(华东, [120, 200], stack总量) bar.add_yaxis(华南, [80, 120], stack总量)同样两个月份每个月的柱子由三个区域叠加而成既能对比总量变化也能大致看出结构比例。这个能力在分区域月度复盘里很实用。4.2 饼图和环形图看占比但别拿它做精细对比饼图在数据可视化圈子里经常被批判但实际业务汇报里它依然很常用。关键要把握好使用边界类别少、且目的是看构成比例时饼图是好选择类别多、或者需要对差异做精细解读时果断换柱状图。pyecharts 的饼图代码from pyecharts.charts import Pie data_pair [ (电子, 632), (家电, 420), (服饰, 518), (食品, 290), ] pie ( Pie(init_optsopts.InitOpts(width700px, height400px)) .add( series_name销售额占比, data_pairdata_pair, radius[35%, 70%], # 环形图内半径35%外半径70% ) .set_series_opts( label_optsopts.LabelOpts(formatter{b}: {d}%) ) ) pie.render(category_pie.html)注意这里的radius参数写成[35%, 70%]就是环形图中间留空后标签可以借助空心地带布局视觉上更干净。formatter{b}: {d}%表示标签显示“品类名 百分比”{b}是名称{d}是占比这个格式化规则在 ECharts 里是通用的。实际经验告诉我当类别超过 6 个时饼图的引导线会乱得像蜘蛛网。遇到这种情况要么把“其它”合并成一类要么换成柱状图。还有一次我需要同时对比两组年份的占比硬用两个饼图并列效果远不如分组柱状图直观。饼图是用来“讲故事”的不是用来“做分析”的这个定位想清楚就行。4.3 散点图看两个变量之间到底有没有关系散点图在分析场景中出现频率很高尤其你想快速判断两个指标的相关性时。ECharts 散点图的代码from pyecharts.charts import Scatter x_data [random.randint(50, 100) for _ in range(50)] y_data [random.randint(100, 200) for _ in range(50)] scatter ( Scatter() .add_xaxis(x_data) .add_yaxis(样本, y_data, symbol_size8) .set_global_opts( title_optsopts.TitleOpts(title散点分布), xaxis_optsopts.AxisOpts(name曝光量), yaxis_optsopts.AxisOpts(name点击量), ) ) scatter.render(scatter_demo.html)如果数据点很多几千上万个我一般会在渲染前做抽样或分箱。散点图适合看趋势方向一旦点太多画成实心黑团反而什么都看不见。ECharts 对大量点的渲染虽说不差但浏览器性能摆在那儿先让数据量降下来是明智的选择。4.4 热力图两个维度交叉时的高密度展示热力图适合“星期几”和“一天中的小时”这样的双维度数据或者品类和地区交叉的密度矩阵。pyecharts 用法from pyecharts.charts import HeatMap hours [str(h) 时 for h in range(24)] days [周一, 周二, 周三, 周四, 周五, 周六, 周日] data [[h, d_index, random.randint(0, 100)] for d_index, d in enumerate(days) for h in range(24)] heatmap ( HeatMap() .add_xaxis(hours) .add_yaxis(活跃度, days, data) .set_global_opts( title_optsopts.TitleOpts(title一周活跃度热力图), visualmap_optsopts.VisualMapOpts(max_100), ) ) heatmap.render(heatmap_demo.html)热力图需要搭配visualmap_opts也就是颜色映射组件。max_可理解为颜色映射的上限。我一般会根据数据分布动态确定上限比如取 95 分位数否则一两个极端值会把整张图拉成单一色调细节全丢。5. 交互不是花哨缩放、悬停、标记、导出是日常操作5.1 tooltip用 axis 触发还是 item 触发别搞反了悬停提示是 ECharts 最讨喜的交互之一但初学者经常忽略触发方式。对折线图和柱状图triggeraxis更合适因为鼠标靠近坐标轴时会同时显示轴上所有系列的值方便对比。对饼图、散点图triggeritem更合理只会显示当前悬停的那一项。tooltip_optsopts.TooltipOpts(triggeraxis, axis_pointer_typecross)axis_pointer_typecross会让十字准线跟随鼠标移动在分析时间趋势时能更精确定位到具体日期。这个配置在大屏或表格页面里特别好用。5.2 datazoom数据一多这个配置就是救命稻草当折线图的时间跨度变大比如 180 天数据你会发现所有点挤在一次完全失去可读性。数据缩放是标准解法datazoom_opts[ opts.DataZoomOpts(type_inside), opts.DataZoomOpts(type_slider), ]type_inside表示直接在图表区域内用鼠标滚轮缩放type_slider则会在图表下方生成一个滑块条便于快速定位时间区间。我在做长时间序列报表时基本都会同时加这两者滑块负责宏观视图滚轮负责细节查看。注意这里的参数名是type_末尾带下划线因为type在 Python 里是内置函数名这个命名约定在 pyecharts 里很普遍很多配置类的关键字都是这么处理的。5.3 markLine 和 markPoint把关键数值直接画在图上业务汇报里最常说的三句话是最高点在哪、最低点在哪、平均值是多少。这些不应该靠肉眼找应该让图表自己标出来.add_yaxis( 销售额, values, markline_optsopts.MarkLineOpts( data[ opts.MarkLineItem(type_average), opts.MarkLineItem(y100), ] ), markpoint_optsopts.MarkPointOpts( data[ opts.MarkPointItem(type_max, name最高), opts.MarkPointItem(type_min, name最低), ] ), )type_average会画出一条平均值参考线opts.MarkLineItem(y100)是自定义目标线比如 100 万业绩门槛。实测下来加了这两条线的图表在会议上的说服力明显更强因为判断依据直接可视化出来了。5.4 toolbox让看图的人自己动手工具箱也是 ECharts 默认交互的一部分。你可以在全局配置里开启.set_global_opts( toolbox_optsopts.ToolboxOpts() )开启后图表右上角会有一排小图标包含数据视图、动态类型切换、框选缩放、还原、保存图片。你不需要写任何额外代码访问者就能自己把图表导出成 PNG。对业务人员来说这个“自己动手导出图片”的功能非常友好我经常在邮件里附一句“右上角可以直接保存图片”省了不少来回沟通。5.5 双坐标轴组合图柱状和折线同时出现实际分析中经常出现两个量纲不同的指标比如“销售额”和“订单量”销售额可能是几百万订单量只有几千。这种情况下单坐标轴会把订单量压成一条贴近底部的直线。解决方法是双 Y 轴柱状图配左轴折线图配右轴bar Bar() bar.add_xaxis(months) bar.add_yaxis(销售额, sales_data) bar.extend_axis(yaxisopts.AxisOpts(name订单量, type_value, positionright)) line Line() line.add_yaxis(订单量, order_data, yaxis_index1) bar.overlap(line)extend_axis多声明一个右轴line里的yaxis_index1表示这个系列挂在右轴下标 1 上最后用bar.overlap(line)把两张图叠在一起。这是做运营看板时几乎必用的组合手法能把两个不同量级的指标放在同一时间轴里对照观察。6. 完整 Demo一张“商品销售看板”的拼装过程6.1 三张核心图表一个纵向下滑页面这里我用一个模拟的商品销售项目做演示数据不是真实业务数据但流程可以照搬。看板需要呈现三块内容各周销售额趋势、品类销售占比、商品销量排名。用Page对象把三张图拼到一个 HTML 文件里from pyecharts.charts import Page, Bar, Line, Pie from pyecharts import options as opts trend_line ( Line() .add_xaxis([第1周, 第2周, 第3周, 第4周]) .add_yaxis(销售额, [54, 70, 65, 88], is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title月度销售额趋势), tooltip_optsopts.TooltipOpts(triggeraxis), datazoom_opts[opts.DataZoomOpts(type_slider)], ) ) category_pie ( Pie() .add( 占比, [[电子, 120], [家电, 85], [服饰, 92], [食品, 60]], radius[35%, 70%], ) .set_global_opts(title_optsopts.TitleOpts(title品类销售占比)) ) rank_bar ( Bar() .add_xaxis([商品A, 商品B, 商品C, 商品D, 商品E]) .add_yaxis(销量(件), [1200, 980, 870, 660, 540]) .reversal_axis() .set_global_opts( title_optsopts.TitleOpts(title商品销量排名), xaxis_optsopts.AxisOpts(name件), ) ) page Page(layoutPage.SimplePageLayout) page.add(trend_line, category_pie, rank_bar) page.render(sales_dashboard.html)打开生成的 HTML三张图按顺序纵向排列每张图都有交互能力。Page.SimplePageLayout是简单纵向布局够用且稳定。如果你要网格布局可以进一步研究Grid组合那个稍复杂一点但控制力更强。6.2 在 Flask 中动态展示图表静态 HTML 适合一次性交付但如果要做成内部系统图表需要跟着参数动态变化通常要接到 Web 框架里。pyecharts 为此提供了嵌入能力核心是利用render_embed()返回可直接插入模板的 HTML 片段。下面是一个基于 Flask 的示意from flask import Flask, render_template_string from pyecharts.charts import Line from pyecharts import options as opts app Flask(__name__) app.route(/dashboard) def dashboard(): line ( Line() .add_xaxis([1月, 2月, 3月]) .add_yaxis(销售额, [120, 180, 150]) .set_global_opts(title_optsopts.TitleOpts(title月度销售)) ) chart_embed line.render_embed() html html headmeta charsetutf-8 /title看板/title/head body h2销售看板/h2 {{ chart_embed | safe }} /body /html return render_template_string(html, chart_embedchart_embed) app.run(debugTrue)这里的关键是模板里要加| safe过滤器否则 Jinja2 会把 HTML 片段转义图表就只剩一堆标签文本了。我最初就是这么踩坑的输出页面里全是div标签的源码。render_embed()适合简单页面如果图表很多建议考虑异步加载避免首页渲染时一次性塞入过多脚本。不过绝大多数内部工具用这个片段嵌入的方式已经足够。6.3 看板开发里值得养成的三个习惯第一数据聚合在 Python 侧完成不要让浏览器端承载脏数据和全量明细。图表是给人看的不是给数据库做查询的该 groupby 就 groupby。第二图表的标题、图例、单位名称尽量写清楚自动化生成的看板最容易缺单位单位一缺数值再准也白搭。第三颜色和主题保持一致不同图表混搭风格会显得不够专业。7. 踩坑记录五类反复出现的报错与解决7.1 旧代码导入报错No module named pyecharts.charts如果你复制了一段代码运行时报ModuleNotFoundError先确认自己的版本。新版 pyecharts 的导入是from pyecharts.charts import Line旧版根本不认识这个路径。解决办法优先级最高的就是升级到新版pip install -U pyecharts升级后旧代码里的from pyecharts import Bar也要改成新的写法。这个坑不是代码问题是版本更迭造成的但搜索到旧资料时特别容易中招。7.2 Jupyter 里运行后什么都不显示常见原因是没有调用render_notebook()只写了脚本模式的render()。render()只负责生成文件在 Notebook 单元格里不会展示。另一个原因是 Notebook 渲染被缓存重启内核一般能解决。还有一个需要注意的如果用的是 JupyterLab 而不是传统 Notebook某些版本对 pyecharts 支持略有差异需要保证 pyecharts 相关扩展正常加载。7.3 中文标签乱码或坐标轴显示不全pyecharts 生成的 HTML 文件默认是 UTF-8 编码中文一般不会出问题。乱码更可能来自数据源没有按 UTF-8 读取比如pd.read_csv读入其他编码时出现UnicodeDecodeError或乱码文本。我的处理方式是在数据读取阶段统一指定编码pd.read_csv(sales.csv, encodingutf-8)坐标轴显示不全的另一个常见原因是分类标签太长图表宽度不够解决办法是旋转标签xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate-30))rotate-30表示标签逆时针旋转 30 度文字斜着排既不遮挡也有空间。7.4 图表白屏只有右上角几个小图标这通常是数据本身的问题。可能原因包括add_yaxis的数据列表里有太多空值或者列表长度和 x 轴不一致。调整方式是先打印查看数据结构print(len(x_data), len(y_data)) print(y_data[:10])长度不一致时把两边对齐再做图表。如果数据里有大量NaN先填零或做插值。别小看这类问题十个白屏案例里有七个是数据对齐问题。7.5 大数据量图表卡顿图表点太多之后悬浮提示会变得迟钝。折线图有一种采样策略可以缓解.add_yaxis(序列名, data, is_smoothFalse, samplinglttb)lttb是一种折线采样算法能在保留曲线轮廓的前提下减少渲染点数。它适用于数据点成千上万的时间序列。如果还不够就在 Python 侧先做聚合比如按小时聚合到按天按天聚合到按周把粒度降下来。7.6 地图类图表调不出地理数据如果你要画地图比如按省份显示数值需要引用地图数据。pyecharts 新版对国内外地图数据支持方式有一些变化有些地图资源需要额外安装数据包才能正常显示。遇到地图空白时我的处理方式是先查当前 pyecharts 版本对应的地图数据要求然后按官方说明安装或导入对应数据。地图这一块不太建议硬记代码因为版本差异比较大以实际环境的官方文档为准。整体用下来pyecharts 的配置体系虽然繁多但它的设计保持了 ECharts 的原有风格学过一次后很容易举一反三。我的个人总结是每种图表都有一套固定的“三步走”——创建图表对象、添加数据和系列、配置全局或系列选项。任何一张图都逃不开这三步。如果你现在正在学习数据可视化不必试图背下所有配置项先把你手头最常汇报的那两张图画熟再从交互配置开始逐项加功能。图表这种东西一旦你在真实场景里跑通一次后面就顺了。