地图可视化的联用——基于Pyecharts与Plotly相结合的方法

QQ截图20220521115513.png

背景:笔者在近期的业务部会议中,发现某些区域及用户一直被反复提及。根据自身对数据的敏感度,做了个日报看板,发现只要产品线同期下降30%,这样的数据反应在折线图上,感官是非常明显的,可以说看上去几乎下降了一半。在强烈数据波动的影响下,笔者为了更好地了解各流失地区、产品线,及相关用户的发展走势,故有了这次数据探索。

技术工具如下:

地图框架:使用Pyecharts的Timeline、Tab模块,对湖南省各个县区数据进行可视化;定义城市的类模块进行各个县区的封装,避免自定义过多的城市;使用Plotly的Scattermapbox模块下钻至各区域具体用户的历年业务状况;通过Beautifulsoup解析渲染出来的html文件,并对多个html文件的拼接。

数据处理核心点:使用melt方法对透视表进行转置;通过cat.reorder_categories方法进行自定义排序,这类似Excel中的自定义排序功能。

1 导入库与数据集

import pandas as pd
import numpy as np
df = pd.read_csv(r'alldata.csv',low_memory=False)
df
image.png

约160万行的数据集。

1.1选择2021年有业务,但2022年没有业务的地区、产品线。

df_hunan = df[df['省份']=='湖南']
cross_hunan = pd.crosstab([df_hunan['城市'],df_hunan['县区'],df_hunan['产品']],df_hunan['年度']).reset_index()
cross_result = cross_hunan[(cross_hunan['2022']==0)&(cross_hunan['2021']!=0)].reset_index(drop=True).sort_values(by=['城市','县区','产品'])
cross_result['2022']=0.1
cross_result
image.png

cross_result这张表很关键,后面会反复涉及。至于为什么将2022年的那一列设为0.1,是为了避免后续展现分页选项卡地图的过程中,当鼠标点至2022年的区域时,内容显示为空白。

1.2 将没有数据的城市、县区、产品作为一个组合,去找对应的用户。

理论上用左连接、右连接等方法可以找到对应的用户,但试了好几次,最后的结果都不正确,所以只能写循环去找用户了。

result_to_crossresult = pd.crosstab([df['用户'],df['城市'],df['县区'],df['产品']],df['年度']).reset_index()
DT = []
for i in range(len(cross_result[['县区','产品']])):
    DT.append(result_to_crossresult[(result_to_crossresult['县区']==list(cross_result[['县区','产品']].values[i])[0])
                &(result_to_crossresult['产品']==list(cross_result[['县区','产品']].values[i])[1])])
Res = pd.concat(DT).reset_index(drop=True)
Res
image.png

1.3 将2019-2022年数量和排序,从而得到地区流失程度由大到小的列表

cityname_list = list((Res.assign(总值 = lambda x:x.loc[:,"2019":"2022"].apply(sum,axis=1))#将近三年总数最多的进行排序
    .sort_values(by='总值',ascending=False)
)['城市'].unique()
    )
cityname_list
image.png

2.处理城市

2.1先看一个城市+时间轴的板块

from pyecharts.charts import Bar,Funnel,Kline,Gauge,WordCloud,Map,Grid
from pyecharts import options as opts
from pyecharts.charts import Page,Pie,Scatter
from pyecharts.charts import Map, Timeline
def time_line_one() -> Timeline:
    t1 = Timeline(init_opts=opts.InitOpts(width="1000px"))
    for year in range(2014, 2023):
        #citylist = []
        for i in range(2014, 2023):
            cityty = '衡阳市'#【查城市】,修改名称
            area_df = cross_result.replace({'宁乡市':'宁乡县','邵东市':'邵东县','城步县':'城步苗族自治县',
                                      '通道县':'通道侗族自治县','靖州县':'靖州苗族侗族自治县','新晃县':'新晃侗族自治县',
                                      '芷江县':'芷江侗族自治县','麻阳县':'麻阳苗族自治县','江华县':'江华瑶族自治县'})
            area_list = list(area_df['县区'])
            area_count = list(area_df['{}'.format(i)])
            map_ = Map()
            map_.add("{}".format(cityty)
                   ,[list(z) for z in zip(area_list, area_count)]
                  , "{}".format(list(pd.Series(cityty).str.replace('市','',regex=True).unique())[0])
                  ,is_roam=True
                 )
            map_.set_global_opts(title_opts=opts.TitleOpts(title="{}{}年数据".format(cityty,i),pos_left='left'),
                                visualmap_opts=opts.VisualMapOpts(min_=0, max_=max(area_count)+1
                                                                  ,range_color=['#F5F5DC', '#DC143C']
                                                                  ,pos_left='right'
                                                                  ,is_piecewise=False),
                                legend_opts=opts.LegendOpts(is_show=False)
                              )
            t1.add(map_, "{}年".format(i))

        return t1
time_line_one().render_notebook()

cross_result有个replace的操作,原因在于要将县区字段改成pyecharts的官方名单,否则数据无法显示。此外,还需注意pd.Series(cityty).str.replace('市','',regex=True)的操作,目的是将城市中带有“市”字段的内容替换为空。


image.png

2.2 解读pyecharts官网的分页选项卡

https://gallery.pyecharts.org/#/Tab/tab_base
根据官网的例子,需要几个类别就要def几次。然而,我们有12个城市,就需要定义12次,如果有100个城市,就需要定义100次。那么代码会变得相当冗长,且不易维护。

image.png

2.2处理城市-定义城市的类模块

from pyecharts import options as opts
from pyecharts.charts import Map, Timeline,Tab
class City():
    def __init__(self,citycity):
        self.citycity = citycity
    
    def time_line_one(self) -> Timeline:
        t1 = Timeline(init_opts=opts.InitOpts(width="1000px"))#init_opts=opts.InitOpts(width="1000px")
        for i in range(2015, 2023):
            cityty = self.citycity#【查城市】,修改名称
            area_df = cross_result.replace({'宁乡市':'宁乡县','邵东市':'邵东县',
                                            '城步县':'城步苗族自治县','通道县':'通道侗族自治县',
                                            '靖州县':'靖州苗族侗族自治县','新晃县':'新晃侗族自治县',
                                             '芷江县':'芷江侗族自治县','麻阳县':'麻阳苗族自治县',
                                            '江华县':'江华瑶族自治县'})
            area_list = list(area_df['县区'])
            area_count = list(area_df['{}'.format(i)])
            map_ = Map()
            map_.add("{}".format(cityty)
                   ,[list(z) for z in zip(area_list, area_count)]
                  , "{}".format(list(pd.Series(cityty).str.replace('市','',regex=True).unique())[0])
                  ,is_roam=True
                 )
            map_.set_global_opts(title_opts=opts.TitleOpts(title="""今年未发生业务区域在{}年的业务状况"""
                               .format(i)
                               ,subtitle=""" {}在2021年发生,但今年未发生业务的县区:{}\n\n项目类型为:{}"""
                                        .format(cityty
                                       ,"、".join(list(cross_result
                                             [cross_result['城市']==
                                              self.citycity]['县区']
                                              .unique()
                                            ))# 将列表转为字符串
                                       ,"、".join(list(cross_result
                                            [cross_result['城市']==
                                             self.citycity]['产品']
                                            .unique()
                                           )))
                               ,subtitle_textstyle_opts={'color':'#696969',
                                                         'font_size':100
                                                         #'font_style':'italic',
                                                         #'font_weight':'bolder'
                                                        }
                               ,pos_left='left'
                                                          ),
                                visualmap_opts=opts.VisualMapOpts(min_=0, max_= max(area_count)+1
                                                                  #,range_color=['#D7E3EF', '#163A69']
                                                                  ,range_color=['#F5F5DC', '#DC143C']
                                                                  ,pos_left='right'
                                                                  ,is_piecewise=False),
                                legend_opts=opts.LegendOpts(is_show=False)
                              )
            t1.add(map_, "{}年".format(i))
        return t1

2.1 渲染带有时间轴(Timeline)的市级地图

tab = Tab()
for i in range(len(cityname_list)):
    
    #tab = Tab()
    tab.add(City(citycity=cityname_list[i]).time_line_one(), cityname_list[i])
#tab.render_notebook()
tab.render('市级地图(阉割).html')
image.png

选项卡的顺序就是cityname_list的列表顺序

2.2 渲染表格

from pyecharts.components import Table

cross_result.drop(labels='2014',axis=1,inplace=True)#如果二次运行,这里会报错,需要注释掉
tab1 = Tab()
for i in range(len(cityname_list)):
    
    headers = list(cross_result.columns)
    cross_result.replace(0.1,0,inplace=True)
    cross_result['2022'] = cross_result['2022'].astype(int)
    rows = cross_result.query("城市=='{}'".format(cityname_list[i])).values
    attributes = {"font-size":"0.1px"} 
    table = (
        Table()
        .add(headers, rows)
        .set_global_opts({
                           "title_style":"style='color:red'",
                           "subtitle_style":"style='color:green'"
                          })
            )
    tab1.add(table,cityname_list[i])
#tab1.render_notebook()
tab1.render('市级数据表格(阉割).html')

2.3渲染Plotly的地图-气泡图(Scattermapbox)

先将数据处理成plotly可读的结构。

Res_ =  pd.melt(Res,id_vars=list(Res.columns[:4])).rename(columns = {'value':'申请次数','variable':'年度'})
Res_
image.png

将Res_表格排序,排序规则为以永州市为首的cityname_list列表顺序。

import pandas as pd
import plotly.express as px
import plotly
import datetime
from datetime import *
import numpy as np
Res_['城市'] = Res_['城市'].astype('category')
Res_['城市'].cat.reorder_categories(cityname_list, inplace=True)
Res_['年度'] = Res_['年度'].astype(int)
Res_.sort_values(by=['城市','年度'],inplace=True)
Res_
image.png
df_lon_lat = pd.read_excel(r'经纬度.xlsx')#读取经纬度
df_lon_lat.drop_duplicates(inplace=True)

plotly_df = (pd.merge(Res_.drop(index= Res_[Res_['年度']==2014].index)
                     ,df_lon_lat,how='left')
                        .assign(申请次数 = lambda x:np.where(x['申请次数']==0,0.1,x['申请次数'])
                    )
            )

px.set_mapbox_access_token("pk.eyJ1IjoidGlua2VyaXNtIiwiYSI6ImNrdDg4czh1djA2eGcyb3A5YXYyMmc3Z2EifQ.vnlyVgi0NCi1c1TcpwVH-g")
title = '今年未产生数据的县区中,用户的历年业务状况'
fig = px.scatter_mapbox(plotly_df
                        ,title = title
                        ,text = '用户'
                        ,hover_name= '产品'
                        ,lat="纬度",lon="经度"
                        ,color="城市"
                        ,size="申请次数"
                        , size_max=50
                        , zoom=10
                        ,animation_frame = '年度'
                       )
fig.show()
plotly.offline.plot(fig, filename=title+".html")

丢弃2014年的数据是因为,这一年中相关用户及对应的产品线均无数据。将每年申请次数为0的改为0.1,原因在于,若数据为0则图例(legend)中的圆圈会变成一个点,不便于查看。


image.png

有些经纬度没对上,回头修改一下。

3 使用Beautifulsoup解析html文件,最终完成html的拼接

目前我们已经生成了3个html文件,现在将它们拼起来。

from bs4 import BeautifulSoup
text = open('市级地图(阉割).html','r',encoding='utf8')
soup_map = BeautifulSoup(text,'lxml')#'lxml'

text2 = open('市级数据表格(阉割).html','r',encoding='utf8')
soup_table = BeautifulSoup(text2,'lxml')

text3 = open('今年未产生数据的县区中,用户的历年业务状况.html','r',encoding='utf8')
soup_bigmap = BeautifulSoup(text3,'lxml')

test_text = """<html>
             <body>
             {}{}{}
             </body>
             </html>
                """
text_merge = test_text.format(soup_map,soup_table,soup_bigmap)
file = open(r'2022年未发生业务地区所对应的用户历年数据.html','w',encoding='utf-8')
file.write(text_merge)
file.close()
gif_pre20225209376.gif
gif_pre202252015199.gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容