前言
-
之前在【Pyecharts Gallery】中看不中用的可视化作品集合~发布过一个可视化作品——微博签到中国,不过当时存在一个比较严重的问题,数据加载太慢了,图表中总共包含了30W+个点,渲染完所有的点都得一两分钟,在图例筛选上也存在非常严重的卡顿,用户体验实在太差,最近得空又重新研究捣鼓了下,终于解决了,这次也尽可能详细的讲解下我的解决办法;
-
Echarts在17年发布了GL,对于量级较大的数据支持性能上有了质的提升,所以理论上我们只要GEO-Scatter图表更换成GEO-scatterGL就能解决问题,不过scatterGL在pyecharts中并不能直接支持,具体怎么实现,我们看下面具体实例。
-
另外由于全国的图表数据量太大,整个图表将近80M,无法在KLab中发布,下面的实例中会以北京市的签到数据为例进行讲解,全国的代码也会在后面附上,各位可以复制到自己本地去运行!
- 全部代码以及数据集都已上传至我的KLAB,各位可以自行取阅~
数据处理
精度处理
将签到位置的经纬度的精度降低,统一取小数点后3位。
原始文件中的经纬度精确到了小数点后3-7位,在现实中可能就是100米左右的偏差(瞎说的,没具体查~),但这距离反映到图表中可能也就几微米的差距,完全可以舍弃掉,而且这样做有以下两个好处:
- 降低精度之后可以对数据进行一次聚合计数,计算出哪些位置是签到人数更多的地方,在可视化图表通过不同颜色来展现,让用户一眼就能发现哪些位置是热门地点;
- 缩小数据量,整个原始数据大概是900W条记录,在降低到后两位精度的话可以减少到30W左右,可以大大的提高图表的加载速度;
df_bj = pd.read_json('/home/kesci/input/weibo_data2887/WeiboDataShare/beijing.json')
# 设置经纬度的精度为小数点后三位
df_bj['lat'] = df_bj['lat'].apply(lambda x: '{:.3f}'.format(x))
df_bj['lon'] = df_bj['lon'].apply(lambda x: '{:.3f}'.format(x))
df_bj.head()
city | lat | location | lon | |
---|---|---|---|---|
0 | 北京 | 24.115 | 丽景花园 | 102.774 |
1 | 北京 | 24.114 | K歌城 | 102.761 |
2 | 北京 | 23.980 | 炸弹葱油饼 | 121.610 |
3 | 北京 | 23.991 | 丽轩国际酒店 | 121.625 |
4 | 北京 | 23.988 | 花莲丽轩大饭店 | 121.629 |
聚合计算
对降低精度之后的数据进行一次聚合计数,计算出每个地点签到的次数;
df_bj = df_bj.groupby(['lon', 'lat'])['city'].count().reset_index()
df_bj.columns = ['lon', 'lat', 'num']
新建GEO实例
因为需要将每个地点位置信息添加到GEO中去,我们在此先建立一个GEO实例,在后面一个cell遍历数据分类的时候,便可以一起将坐标位置信息添加到GEO中了~
geo = Geo(init_opts=opts.InitOpts(theme='dark', bg_color='#000000', width='1000px', height='800px'))
地点分类
将所有地点根据签到人次分为强、中、弱三类,同时将地点的经纬度信息添加到GEO实例中去~
# 根据位置签到次数分为强中弱三个级别
weak, strong, normal = [], [], []
for idx, row in df_bj.iterrows():
if row.num < 4:
weak.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
elif 4 <= row.num < 10:
normal.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
elif row.num >= 10:
strong.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
print(f'弱分类地点数:{len(weak)}\n中分类地点数:{len(normal)}\n强分类地点数:{len(strong)}')
可视化代码
大部分代码与GEO-Scatter的配置一样,这边就不再赘述了,可以参见我以前的两个项目,里面有简单的代码实例:
这边着重说一下几个特殊的地方:
Scatter-GL的使用
以下为官方文档中Geo.add()方法的注释:
def add(
# 系列名称,用于 tooltip 的显示,legend 的图例筛选。
series_name: str,
# 数据项 (坐标点名称,坐标点值)
data_pair: Sequence,
# Geo 图类型,有 scatter, effectScatter, heatmap, lines 4 种,建议使用
# from pyecharts.globals import GeoType
# GeoType.GeoType.EFFECT_SCATTER,GeoType.HEATMAP,GeoType.LINES
type_: str = "scatter",
# 其他部分省略,可以参见文档http://pyecharts.org/#/zh-cn/geography_charts
)
在官方代码中Geo只支持4中类型,分别是scatter(散点图),effectScatter(带涟漪效果的散点图),heatmap(热力图)和lines(线图),Scatter-GL在pyecharts中是不支持直接使用的,那如果我们强行通过geo.add( type_ = 'scatter')
使用行吗?结果是不行的,我们可以参看Pyecharts中Geo.add()
的源码pyecharts/charts/basic_charts/geo.py:
- type_部分处理代码:
if type_ == ChartType.SCATTER:
self.options.get("series").append(
{
# 代码太长了,具体配置项省略,可以直接去github查看
}
)
elif type_ == ChartType.EFFECT_SCATTER:
self.options.get("series").append(
{
# 代码太长了,具体配置项省略,可以直接去github查看
}
)
elif type_ == ChartType.HEATMAP:
self.options.get("series").append(
{
# 代码太长了,具体配置项省略,可以直接去github查看
}
)
elif type_ == ChartType.LINES:
self.options.get("series").append(
{
# 代码太长了,具体配置项省略,可以直接去github查看
}
)
elif type_ == ChartType.CUSTOM:
self.options.get("series").append(
{
# 代码太长了,具体配置项省略,可以直接去github查看
}
)
如果你传入了除scatter, effectScatter, heatmap, lines, custom
这五种以外的参数,程序根本就不会处理,所以如果要使用scatter-GL
我们便得另谋出路了。
继续往下说之前,我们先普及一下概念,pyecharts在整个可视化过程中充当的只是一个“翻译”的作用,他只是将你的Python代码翻译成echarts能够使用的配置信息,所有的图表生成都是通过echarts来完成的,所以理论上我们并不需要按部就班的使用Pyecharts中的方法,只要自己能写清楚各项配置也是可行的;
- 可以找个简单的例子将图表的option打印出来,各位感受一下:
# 虚假数据
x_data = ['Apple', 'Huawei', 'Xiaomi', 'Oppo', 'Vivo', 'Meizu']
y_data = [123, 153, 89, 107, 98, 23]
bar = (Bar()
.add_xaxis(x_data)
.add_yaxis('', y_data)
)
# 打印Bar的option
print(bar.dump_options())
- 打印结果如下:
{
"animation": true,
"animationThreshold": 2000,
"animationDuration": 1000,
"animationEasing": "cubicOut",
"animationDelay": 0,
"animationDurationUpdate": 300,
"animationEasingUpdate": "cubicOut",
"animationDelayUpdate": 0,
"color": [
"#c23531",
"#2f4554",
"#61a0a8",
"#d48265",
"#749f83",
"#ca8622",
"#bda29a",
"#6e7074",
"#546570",
"#c4ccd3",
"#f05b72",
"#ef5b9c",
"#f47920",
"#905a3d",
"#fab27b",
"#2a5caa",
"#444693",
"#726930",
"#b2d235",
"#6d8346",
"#ac6767",
"#1d953f",
"#6950a1",
"#918597"
],
"series": [
{
"type": "bar",
"legendHoverLink": true,
"data": [
123,
153,
89,
107,
98,
23
],
"showBackground": false,
"barMinHeight": 0,
"barCategoryGap": "20%",
"barGap": "30%",
"large": false,
"largeThreshold": 400,
"seriesLayoutBy": "column",
"datasetIndex": 0,
"clip": true,
"zlevel": 0,
"z": 2,
"label": {
"show": true,
"position": "top",
"margin": 8
}
}
],
"legend": [
{
"data": [
""
],
"selected": {
"": true
}
}
],
"tooltip": {
"show": true,
"trigger": "item",
"triggerOn": "mousemove|click",
"axisPointer": {
"type": "line"
},
"showContent": true,
"alwaysShowContent": false,
"showDelay": 0,
"hideDelay": 100,
"textStyle": {
"fontSize": 14
},
"borderWidth": 0,
"padding": 5
},
"xAxis": [
{
"show": true,
"scale": false,
"nameLocation": "end",
"nameGap": 15,
"gridIndex": 0,
"inverse": false,
"offset": 0,
"splitNumber": 5,
"minInterval": 0,
"splitLine": {
"show": false,
"lineStyle": {
"show": true,
"width": 1,
"opacity": 1,
"curveness": 0,
"type": "solid"
}
},
"data": [
"Apple",
"Huawei",
"Xiaomi",
"Oppo",
"Vivo",
"Meizu"
]
}
],
"yAxis": [
{
"show": true,
"scale": false,
"nameLocation": "end",
"nameGap": 15,
"gridIndex": 0,
"inverse": false,
"offset": 0,
"splitNumber": 5,
"minInterval": 0,
"splitLine": {
"show": false,
"lineStyle": {
"show": true,
"width": 1,
"opacity": 1,
"curveness": 0,
"type": "solid"
}
}
}
]
}
在pyecharts中options并非一个私有变量,我们是可以自行更改的,只要我们改完后的配置(options)在echarts能够使用就行;
我们可以通过如下代码来将GEO的类型更改为scatter-GL
:
geo.add("",
data,
type_='scatter')
# 将GEO的type更改为scatterGL
geo.options['series'][0]['type'] = 'scatterGL'
这样第一部分问题便解决了~
添加JS依赖
通过上述步骤配置之后,代码会执行成功,文件也会生成,不过当你打开图表html文件的时候还是会一片空白。
定位问题
在前文中提到了,pyecharts中只是一个翻译的角色,它只会管你输入的参数是不是合法,但这些参数能否起作用,pyecharts并不会知道,如果在图表生成渲染中存在问题(echarts的报错),在pyecharts中是不会产生报错的;所以我们在定位问题的时候,除了查看Python下的报错外,还可以生成html文件到本地,打开后然后F12选择console查看报错,这里面便能看到Echarts产生的报错;
我们打开生成的html文件,然后F12查看下console,可以看到如下报错:Uncaught Error: Component series.scatterGL not exists. Load it first.
虽然不咋会用JS,但也大概能看出是出了什么问题:
其实就是我们在代码中使用了scatterGL,但并未引用它,就相当于在Python中,我们代码中使用了pandas.read_csv()
,但并未在使用之前import pandas
。
我们切换到elements的tab下查看页面代码:
可以看到,页面中只引用了echarts.min.js
,echarts.min.js
只包含了一些常规的图表,如果要使用scatterGL,我们需要添加echarts-gl.min.js
的引用,在Python中可以通过如下代码添加引用:
geo.js_dependencies.add("echarts-gl")
这样我们就能正常使用scatterGL图了,完整代码如下:
geo.add_schema(maptype="北京", is_roam=True, zoom=1.1,
itemstyle_opts=opts.ItemStyleOpts(color="#000000", border_color="#1E90FF"),
emphasis_label_opts=opts.LabelOpts(is_show=False),
emphasis_itemstyle_opts=opts.ItemStyleOpts(color="#323c48"))
geo.add("弱",
weak,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#1E90FF"))
geo.add("中",
normal,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#00FFFF"))
geo.add("强",
strong,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#E1FFFF"))
geo.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
geo.set_global_opts(
title_opts=opts.TitleOpts(title="微博签到点亮中国", pos_top='top', pos_left='center'),
tooltip_opts=opts.TooltipOpts(is_show=False),
legend_opts=opts.LegendOpts(is_show=True, pos_left='left', orient='vertical'))
# 添加依赖,scatterGL需要使用
geo.js_dependencies.add("echarts-gl")
# 更改geo图类型
geo.options['series'][0]['type'] = 'scatterGL'
geo.options['series'][1]['type'] = 'scatterGL'
geo.options['series'][2]['type'] = 'scatterGL'
# geo.render('/home/kesci/work/test_beijing.html')
geo.render_notebook()
全国数据代码
# 读取全国数据,替换成自己的路径
df = pd.read_csv(r'/home/kesci/input/weibo_data2887/WeiboDataShare/weibo_s_size.csv')
df.head()
# 新建GEO实例
geo = Geo(init_opts=opts.InitOpts(theme='dark', bg_color='#000000', width='1000px', height='800px'))
# 将数据分为强中弱三类
weak, strong, normal = [], [], []
for idx, row in df.iterrows():
if row.num < 10:
weak.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
elif 10 <= row.num < 30:
normal.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
elif row.num >= 30:
strong.append((idx, row.num))
geo.add_coordinate(idx, row.lon, row.lat)
# 设置地图
geo.add_schema(maptype="china", is_roam=False, zoom=1.2,
itemstyle_opts=opts.ItemStyleOpts(color="#000000", border_color="#1E90FF"),
emphasis_label_opts=opts.LabelOpts(is_show=False),
emphasis_itemstyle_opts=opts.ItemStyleOpts(color="#323c48"))
# 添加数据
geo.add("弱",
weak,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#1E90FF"))
geo.add("中",
normal,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#00FFFF"))
geo.add("强",
strong,
type_='scatter',
is_selected=True,
symbol_size=1,
is_large=True,
itemstyle_opts=opts.ItemStyleOpts(color="#E1FFFF"))
# 关闭标签
geo.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
geo.set_global_opts(
title_opts=opts.TitleOpts(title="微博签到点亮中国", pos_top='top', pos_left='center'),
tooltip_opts=opts.TooltipOpts(is_show=False),
legend_opts=opts.LegendOpts(is_show=True, pos_left='left', orient='vertical'))
# 添加依赖
geo.js_dependencies.add("echarts-gl")
# 更改图表类型
geo.options['series'][0]['type'] = 'scatterGL'
geo.options['series'][1]['type'] = 'scatterGL'
geo.options['series'][2]['type'] = 'scatterGL'
geo.render()