手动反爬虫:原博地址 https://blog.csdn.net/lys_828/article/details/128605640
知识梳理不易,请尊重劳动成果,文章仅发布在CSDN网站上,在其他网站看到该博文均属于未经作者授权的恶意爬取信息
整个项目的页面包含四部分,由头部信息,卡片数据,多图表以及动态地图可视化组成,项目代码已上传 个人Github仓库。
assets文件夹中就是加载的样式和图片,样式这里可以使用之前已经下载好的bootstrap.min.css样式(也可以按照 项目6.2bootstrap组件 中的介绍下载其它的样式模板),图片就是Covid的一个模型图片,对应如下。
主框架就是app.py项目初始化文件和index.py主程序运行文件,其中app.py文件中的信息较为简单,就是创建一个dash的应用,代码如下
from dash import Dash
app = Dash('__name__')
index.py文件中引入初始化后的应用,然后进行布局及运行初始化设置。
from app import app
import dash_bootstrap_components as dbc
from header import HeaderInfo
from cards import CardInfo
from indicators import IndicatorInfo
from piechart import PieInfo
from barlinechart import BarLineInfo
from mapchart import MapInfo
app.layout = dbc.Col(
[
dbc.Row([HeaderInfo()]),
dbc.Row([CardInfo()]),
dbc.Row([IndicatorInfo(),PieInfo(),BarLineInfo()]),
dbc.Row([MapInfo()]),
]
)
if __name__ == '__main__':
app.run_server(debug=True)
然后在对应的文件中创建函数,搭建基本的框架,各文件的基础信息如下。
项目的头部信息包含了三个部分,可以按照一行三列的布局设置,第一列放置图片居左,第二列放置标题居中,第三列放置更新时间居右。
import dash_bootstrap_components as dbc
def HeaderInfo():
return dbc.Row(
[
dbc.Col([None]),
dbc.Col([None]),
dbc.Col([None]),
]
)
其中图片的加载和位置设置,以及标题文字的大小及距离的设置相对简单,最主要解决第三列更新时间的问题,首先给定一个今天的时间作为样例,后续根据接口数据获取的具体时间再更换,在api.ipynb文件中将时间转化为目标样式的代码如下。
import pandas as pd
today = pd.to_datetime('today').strftime('%B %d %Y')
today
将上述代码复制粘贴到api.py文件中并通过语句导入到header.py文件,完善此文件的信息,代码如下。
import dash_bootstrap_components as dbc
from dash import html
from api import today
def HeaderInfo():
return dbc.Row(
[
dbc.Col(
[
html.Img(src='assets/corona-logo-1.jpg',height='100px',width='100px')
],style = {'textAlign':'left','paddingLeft':'6rem','paddingTop':'1rem'}
),
dbc.Col(
[
html.H1('Covid - 19'),
html.H6('Track Covid - 19 Cases',className='p-2')
],style = {'textAlign':'center','paddingTop':'1rem'}
),
dbc.Col(
[
html.H5(f'Last Updated: {today} (UTC)'),
],style = {'textAlign':'right','paddingRight':'6rem','paddingTop':'3rem'}
),
]
)
保存修改后,回到index.py文件执行,并刷新网址:http://127.0.0.1:8050/。此时界面输出内容如下:
左右两侧的距离浏览器的距离也太远了,问题就在于最开始使用了dbc.Container()
容器,在默认加载css样式中,导致添加的内容自动居中,因此可以把容器调整为html.Div()
,修改后的index.py文件中的代码内容如下:
通过观察最终的图片样式,卡片数据部分的内容基本布局一致,包含了上中下三个部分,第一部分就是文字说明统计类型,中间为该类型的数据,最下面是增加数量和百分比。可封装一个函数card()
,保留数据接口,设置参数为type
,num
,diff
,diff_per
四个参数,先用简单示例数据进行赋值,设置完毕后,再处理数据。
def card(type,num,diff,diff_per,color):
return dbc.Col(
[
html.H6(f'Global {type}'),
html.H3(num,style={'color':color}),
html.Span(f'New {diff} ({diff_per})%',style={'color':color})
]
)
接着完善该文件,此时全部代码如下。
其中api.py文件是为提供数据接口,api.iypnb文件方便进行数据处理和调式工作。前面已经用来处理更新的时间格式,这里就针对真实的Covid数据进行数据导入。此项目使用的数据来自于约翰霍普金斯大学系统科学与工程中心(JHU CSSE)运营的2019年新型冠状病毒可视化仪表盘的数据存储库,数据来源网址:COVID-19
Covid数据中的确诊、死亡、治愈信息所在网址:
url_confirmed = 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv'
url_deaths = 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_deaths_global.csv'
url_recovered = 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_recovered_global.csv'
使用pandas可以直接读取网址链接中的数据,但是前提是用户可以正常访问github。如果无法正常访问Github网址,后续会把读取到的数据保存到本地,并放置在assets文件夹下新建的data文件夹下, 读者可以在下载项目案例后使用本地csv文件读取数据。读取数据的代码如下:
#读取数据
confirmed = pd.read_csv(url_confirmed)
deaths = pd.read_csv(url_deaths)
recovered = pd.read_csv(url_recovered)
数据存放在confirmed
、deaths
、recovered
三个变量中,分别查看前5条数据,输出结果如下:
根据输出结果发现对于治愈人数最近的时期中前5行数据均为0,需要核实一下是不是所有的国家的数值均为0,如果是,说明对于治愈的数据该数据库就不再进行统计。比如选取最近20天的数据,进行数值汇总,发现输出结果中个字段的总和均为0,说明该数据库已经不再更新治愈数据。
考虑进一步提取有治愈数据的信息,首先确定何时该数据库不再统计此部分数据。此处判断的方式就是对每个数值字段进行求和汇总,如果连续5个字段的汇总值都是0,说明连续5个字段中的第一个字段就是数据库停止更新的日期。
ls = recovered[recovered.columns[4:]].sum().to_list()
for i in range(len(ls)):
if ls[i] == ls[i+1] == ls[i+2] == ls[i+3] == ls[i+4]:
print(i)
break
输出结果如下:(字段计算是从第5列开始,因为前4个字段都不是数值字段)
输出结果中显示从第561+4也就是565个字段位置开始停止数据更新,可以进一步核实情况,对该字段的前5个字段和后5个字段计数汇总输出如下。
为了更严谨一些,可以对2021年8月5日往后的数据进行汇总值计算,核实是否为0。结果为-8,有点出人意料,进一步,找出汇总值不为0的字段的统计值,发现结果都是-1,代表着未收录数据。因此可以断定治愈数据在2021年8月5日停止更新。
可以把数据保存到本地assets文件夹下的data文件夹中,由于没有data文件夹,需要先创建,可以手动在assets文件夹中创建,也可以使用代码进行创建,并把数据保存到此文件夹中,代码如下及输出结果如下。
#首先把执行路劲转换到项目的assets文件夹下
import os
os.chdir(r'D:\Data Science\plotly学习\个人\【10. Covid data visualizing】Dropdown-pie-line-map-indicator-bar\assets')
#判断该路径下是否有data文件夹,如果没有就创建
if not os.path.exists('data'):
os.mkdir('data')
#把数据生成到对应的文件中,并取消index列,这里index就是数值序号
confirmed.iloc[:,:565].to_csv('data/confirmed.csv',index=False)
deaths.iloc[:,:565].to_csv('data/death.csv',index=False)
recovered.iloc[:,:565].to_csv('data/recovered.csv',index=False)
重新读取本地文件数据(当然也可以直接使用iloc[:,:565]
获取的数据。由于已经在data文件生成了数据文件,所以在api.py文件中就不需要再重新获取数据,直接读入数据就行,此处的重新读取本地文件数据就是为了方便把代码直接复制粘贴到api.py文件中)
由于获取的数据都是“长数据”,需要转化为“短数据”,即把全部日期的字段变成一个字段,实现数据的逆透视。借助pandas下的melt()
函数功能可以实现。
#数据逆透视:把在标题上的信息转化成为单元格中信息(即行转列)
total_confirmed = pd.melt(confirmed,id_vars=confirmed.columns[:4],value_vars=confirmed.columns[4:],var_name='date',value_name='confirmed')
total_deaths = pd.melt(deaths,id_vars=deaths.columns[:4],value_vars=deaths.columns[4:],var_name='date',value_name='death')
total_recovered = pd.melt(recovered,id_vars=recovered.columns[:4],value_vars=recovered.columns[4:],var_name='date',value_name='recovered')
输出结果如下:(函数中的第一个参数是要进行转化的DataFrame,第二个参数就是指定的不变的字段,第三个参数就是变化的字段,第四个参数就是个变化的字段进行合并成一个字段后的字段名称,最后一个参数就是值汇总后的字段名称)
同样也可以输出转化后的死亡和治愈数据,但是发现治愈数据的记录条数要比确诊和死亡的数据量要少一些。
为了保证同一性,把三个数据进行合并,最终的数据量以死亡和确诊数据量为准,把治愈数据合并进来,最终形成完整数据covid_data
。
#数据合并
covid_data = total_confirmed.merge(right = total_deaths, how = 'left', on = ['Province/State', 'Country/Region', 'date', 'Lat', 'Long'])
covid_data = covid_data.merge(right = total_recovered, how = 'left', on = ['Province/State', 'Country/Region', 'date', 'Lat', 'Long'])
covid_data
代码及输出结果如下。
接着创建衍生字段,也就是项目卡片数据中的第四个卡片的内容数据,即active
,该字段的数据是确诊数据-死亡数据-治愈数据。由于治愈字段在合并之前没有那么多数据量,所以在合并后系统默认以缺失值标记,因此在创建字段之前需要先处理缺失值。
#缺失值处理
covid_data['recovered'] = covid_data['recovered'].fillna(0)
#衍生字段创建
covid_data['active'] = covid_data['confirmed'] - covid_data['death'] - covid_data['recovered']
covid_data
代码及输出结果如下。
按照日期字段进行汇总,求解comfirmed、death、recovered和active字段的数据。
#将日期转化为时间格式(会自动进行解析)
covid_data['date'] = pd.to_datetime(covid_data['date'])
#获取按照日期汇总得到的确诊,死亡和治愈和active的人员
covid_data_1 = covid_data.groupby('date')[['confirmed','death','recovered','active']].sum().reset_index()
covid_data_1
代码及输出结果如下:(为了方便后续的时间字段的操作,可以将date字段转变datetime数据类型)
卡片数据中的汇总数据就得到了,只需要获取最后一行数据即可,接着就是求解卡片下方的增加值和百分比数据。
#获取最近一天的确诊,死亡和治愈和active的人员值
confirmed_num = covid_data_1['confirmed'].iloc[-1]
rate_confirmed = (confirmed_num - covid_data_1['confirmed'].iloc[-2])
death_num = covid_data_1['death'].iloc[-1]
rate_death = death_num - covid_data_1['death'].iloc[-2]
recovered_num = 'No data available' if covid_data_1['recovered'].iloc[-1] ==0 else covid_data_1['recovered'].iloc[-1]
rate_recovered = 'No data available' if recovered_num == 'No data available' else recovered_num - covid_data_1['recovered'].iloc[-2]
active_num = covid_data_1['active'].iloc[-1]
rate_active = active_num - covid_data_1['active'].iloc[-2]
代码及输出结果如下:(百分比数据是基于倒数第二行和倒数第一行之间数据之比,但是需要强调的是,数据为0 代表着没有数据可获取,此外,增幅百分比的计算,需要考虑分母为0的情况,因此百分比计算就不放在接口文件中,而是在cards.py进行计算)
在api.ipynb文件中核实无误后,把代码转移到api.py文件中,此时api.py文件中的代码如下。
import pandas as pd
today = pd.to_datetime('today').strftime('%B %d %Y')
import os
os.chdir(r'D:\Data Science\plotly学习\个人\【10. Covid data visualizing】Dropdown-pie-line-map-indicator-bar\assets')
confirmed = pd.read_csv('data/confirmed.csv')
deaths = pd.read_csv('data/death.csv')
recovered = pd.read_csv('data/recovered.csv')
#数据逆透视:把在标题上的信息转化成为单元格中信息(即行转列)
total_confirmed = pd.melt(confirmed,id_vars=confirmed.columns[:4],value_vars=confirmed.columns[4:],var_name='date',value_name='confirmed')
total_deaths = pd.melt(deaths,id_vars=deaths.columns[:4],value_vars=deaths.columns[4:],var_name='date',value_name='death')
total_recovered = pd.melt(recovered,id_vars=recovered.columns[:4],value_vars=recovered.columns[4:],var_name='date',value_name='recovered')
#数据合并
covid_data = total_confirmed.merge(right = total_deaths, how = 'left', on = ['Province/State', 'Country/Region', 'date', 'Lat', 'Long'])
covid_data = covid_data.merge(right = total_recovered, how = 'left', on = ['Province/State', 'Country/Region', 'date', 'Lat', 'Long'])
#缺失值处理
covid_data['recovered'] = covid_data['recovered'].fillna(0)
#衍生字段创建
covid_data['active'] = covid_data['confirmed'] - covid_data['death'] - covid_data['recovered']
#将日期转化为时间格式(会自动进行解析)
covid_data['date'] = pd.to_datetime(covid_data['date'])
#获取按照日期汇总得到的确诊,死亡和治愈和active的人员
covid_data_1 = covid_data.groupby('date')[['confirmed','death','recovered','active']].sum().reset_index()
#获取最近一天的确诊,死亡和治愈和active的人员值
confirmed_num = covid_data_1['confirmed'].iloc[-1]
rate_confirmed = (confirmed_num - covid_data_1['confirmed'].iloc[-2])
death_num = covid_data_1['death'].iloc[-1]
rate_death = death_num - covid_data_1['death'].iloc[-2]
recovered_num = 'No data available' if covid_data_1['recovered'].iloc[-1] ==0 else covid_data_1['recovered'].iloc[-1]
rate_recovered = 'No data available' if recovered_num == 'No data available' else recovered_num - covid_data_1['recovered'].iloc[-2]
active_num = covid_data_1['active'].iloc[-1]
rate_active = active_num - covid_data_1['active'].iloc[-2]
然后将api.py中的数据导入到cards.py文件和header.py文件中,进行数据替换。首先更换header.py文件中的数据更新时间,为简化header.py文件内容,数据处理工作在api.py文件中完成后再传入,保证后续使用时候只需要修改数据接口文件,而不用动其它文件。在api.py文件中添加如下代码获取数据最近的更新时间。
#更新header.py中的数据获取时间
updata_time = covid_data_1['date'].iloc[-1].strftime('%B %d %Y')
然后把update_time
导入到header.py文件中,顺带修改一下文件中样式,使之和最终效果的布局一致。
import dash_bootstrap_components as dbc
from dash import html
from api import updata_time
def HeaderInfo():
return dbc.Row(
[
dbc.Col(
[
html.Img(src='assets/corona-logo-1.jpg',height='100px',width='100px')
],style = {'textAlign':'left','paddingLeft':'6rem','paddingTop':'1rem'}
),
dbc.Col(
[
html.H1('Covid - 19'),
html.H6('Track Covid - 19 Cases',className='p-2')
],style = {'textAlign':'center','paddingTop':'1rem'}
),
dbc.Col(
[
html.H5(f'Last Updated: {updata_time}'),
html.H5('00:01(UTC)',style={'paddingRight':'5rem'})
],style = {'textAlign':'right','paddingRight':'6rem','paddingTop':'2rem'}
),
]
)
保存文件修改后,刷新网址,当前界面中头部信息更新时间完美实现数据更新。
接着处理卡片中的数据和样式。考虑到缺失值的问题,在card()
函数中删掉百分比参数diff_per
,而是进一步判断后再确定,防止因缺失数据导致程序运行保存的现象,最终cards.py中的代码如下。
from dash import html
import dash_bootstrap_components as dbc
from api import *
def card(type,num,diff,color):
if num == 'No data available':
diff_per = 'No data available'
else:
diff_per = diff/num * 100
return dbc.Col(
[
html.H6(f'Global {type}'),
html.H2(num,style={'color':color}),
html.P(f'New {int(diff)} ({diff_per:.2f})%',style={'color':color,'fontSize':5})
]
)
def CardInfo():
return dbc.Row(
[
card('cases',confirmed_num,rate_confirmed,'orange'),
card('death',death_num,rate_death,'red'),
card('recovered',recovered_num,rate_recovered,'green'),
card('active',active_num,rate_active,'black')
],style = {'textAlign':'center','paddingTop':'1rem'}
)
该文件中主要是由Dropdown组件搭配着Indicators图像构成,搭建主体框架,使之满足正常功能。先把第三部分的布局完善,该部分可以分为1行3列,在三个函数中均输入如下内容,查看是否水平排列。
保存文件修改后,刷新网页,此时界面上输出结果如下。
此时本应三列的内容,变成三行输出了, 因此需要在index.py文件中修改一下配置,在第三部分添加一个行设置,如下
此时保存文件后再刷新网址,发现布局样式满足一行三列的排布。
布局解决后,开始处理indicators.py文件中的功能完善,基础先把文字和Dropdown组件信息设置出来,由于indicators图像是回调产生,因此只设置一个容器增加id即可,后续设置回调函数即可。
首先获取Dropdown组件的标签信息,此处选择的是国家或者地区,因此可以在covid_data
中获取Country/Region
字段的唯一值。特别要注意标签信息不能包含None
值,不然程序会报错,所以在构建Dropdown组件时,特别留意列表中不包含None
值。
然后把这部分代码复制粘贴到api.py文件中,再导入到indicators.py文件中。完善Dropdown组件和文字信息,代码如下
import dash_bootstrap_components as dbc
from dash import html,Input,Output,dcc
from api import country_ls,updata_time
def IndicatorInfo():
return dbc.Col(
[
html.H6('Select Country:'),
dcc.Dropdown(
country_ls,
'China',
id = 'dpd'
),
html.H6(f'New cases: {updata_time}'),
dcc.Graph(id='fig1')
],width=3,style={'textAlign':'center'}
)
保存文件后,刷新网页,此时页面显示结果如下
正常显示后,可以设置回调函数,输出Indicators图形,比如先以中国的确诊数据为例,绘制出对比数据更新日期当天的indicator图形(即今天相对于昨天的涨跌指标图),借鉴官方的示例代码。
有了图像示例代码后,就需要构建数据接口,在api.ipynb文件中获取每个国家/地区每天的疫情信息,代码及输出结果如下。
以中国为例,数据更新之日的新增确诊数据和较昨日新增的确诊数据获取方式如下。
today_new_add = covid_data_2[covid_data_2['Country/Region'] == 'China'].iloc[-1]['confirmed'] - covid_data_2[covid_data_2['Country/Region'] == 'China'].iloc[-2]['confirmed']
yesterday_new_add = covid_data_2[covid_data_2['Country/Region'] == 'China'].iloc[-2]['confirmed'] - covid_data_2[covid_data_2['Country/Region'] == 'China'].iloc[-3]['confirmed']
print('今日新增:{},昨日新增{},indicator指标{}'.format(today_new_add,yesterday_new_add,today_new_add-yesterday_new_add))
进一步将获取的过程封装为函数indicator()
,传入国家country
和covid数据类型type
,最后函数返回就是要绘制的figure对象。先把获取每个国家/地区每天的信息covid_data_2
变量复制粘贴到api.py文件汇总,然后导入到indicators.py文件里。
此时indicators.py文件中的代码如下。
import dash_bootstrap_components as dbc
from dash import html,Input,Output,dcc
from api import country_ls,updata_time,covid_data_2
import plotly.graph_objects as go
from app import app
def IndicatorInfo():
return dbc.Col(
[
html.H6('Select Country:'),
dcc.Dropdown(
country_ls,
'China',
id = 'dpd'
),
html.H6(f'New cases: {updata_time}'),
dcc.Graph(id='confirmed'),
dcc.Graph(id='death'),
dcc.Graph(id='recovered'),
dcc.Graph(id='active'),
],width=3,style={'textAlign':'center'}
)
def indicator(country,type):
today_new_add = covid_data_2[covid_data_2['Country/Region'] == country].iloc[-1][type] - covid_data_2[covid_data_2['Country/Region'] == country].iloc[-2][type]
yesterday_new_add = covid_data_2[covid_data_2['Country/Region'] == country].iloc[-2][type] - covid_data_2[covid_data_2['Country/Region'] == country].iloc[-3][type]
return go.Figure(go.Indicator(
title = {'text': f'New {type}'},
mode = "number+delta",
value = today_new_add,
delta = {'position': "bottom", 'reference': yesterday_new_add},
domain = {'x': [0, 1], 'y': [0, 1]}))
@app.callback(
[Output('confirmed','figure'),Output('death','figure'),Output('recovered','figure'),Output('active','figure')],
[Input('dpd','value')]
)
def update(value):
return (
indicator(value,'confirmed'),
indicator(value,'death'),
indicator(value,'recovered'),
indicator(value,'active'),
)
保存文件后刷新网址,界面运行结果如下。(此时三个indicator图像均可以正常加载,点击下拉菜单可以实现数据更新,但是样式和大小需要进一步调整)
对于两个数值的大小和位置,可以通过number和delta两个参数进行控制。
修改后保存,刷新网址,此时界面显示出数值的大小适合,但是标题太小了,因此需要再修改一下标题样式。
标题颜色也需要进行修改,因此需要在indicator()函数中添加一个color参数。此次修改的地方如下:
import dash_bootstrap_components as dbc
from dash import html,Input,Output,dcc
from api import country_ls,updata_time,covid_data_2
import plotly.graph_objects as go
from app import app
ls = ['confirmed','death','recovered','active']
def IndicatorInfo():
return dbc.Col(
[
html.H6('Select Country:',style={'paddingTop':20}),
dcc.Dropdown(
country_ls,
'China',
id = 'dpd'
),
html.H6(f'New cases: {updata_time}',style={'paddingTop':20}),
html.Div(
[
dcc.Graph(id=type,config={'displayModeBar': False}) for type in ls
],
)
],width=3,style={'textAlign':'center','paddingLeft':45,'paddingRight':45}
)
def indicator(country,type,color):
today_new_add = covid_data_2[covid_data_2['Country/Region'] == country].iloc[-1][type] - covid_data_2[covid_data_2['Country/Region'] == country].iloc[-2][type]
yesterday_new_add = covid_data_2[covid_data_2['Country/Region'] == country].iloc[-2][type] - covid_data_2[covid_data_2['Country/Region'] == country].iloc[-3][type]
fig = go.Figure(go.Indicator(
title = {'text': f'New {type}','font': {'size': 15,'color':color}},
mode = "number+delta",
number = {'valueformat': ',',
'font': {'size': 15,'color':color}},
value = today_new_add,
delta = {'position': "right", 'reference': yesterday_new_add,'valueformat': ',',
'relative': False,
'font': {'size': 15}},
domain = {'x': [0, 1], 'y': [0, 1]}))
fig.update_layout(
height = 75 #这个地方卡了很久,如果添加weight属性就不会居中,所以直接去掉就可
)
return fig
@app.callback(
[Output(type,'figure') for type in ls],
[Input('dpd','value')]
)
def update(value):
return (
indicator(value,'confirmed','orange'),
indicator(value,'death','red'),
indicator(value,'recovered','green'),
indicator(value,'active','black')
)
此文件功能就是对Dropdown组件选中的标签,进行疫情四项指标的占比输出。主要的问题在于获取各项指标数据,由于各指标名称和字段名称是相同的,所以还是使用列表推导式的方式进行简化代码。构建pie图核心参数一个是values
,还有一个names
,这两参数都与四个指标相关,最终的代码如下。
import dash_bootstrap_components as dbc
from dash import html,Input,Output,dcc
from app import app
from api import covid_data_2
import plotly.express as px
ls = ['confirmed','death','recovered','active']
def PieInfo():
return dbc.Col(id = 'pie',width=4,
style={'textAlign':'center','paddingTop':20})
@app.callback(
Output('pie','children'),
[Input('dpd','value')]
)
def update(value):
lastest_record = covid_data_2[covid_data_2['Country/Region'] == value].iloc[-1]
fig = px.pie(values=[lastest_record[i] for i in ls],names=ls)
fig.update_layout(
legend={
'orientation':'h',
'xanchor':'center',
'yanchor':'bottom',
'x':0.5,
'y':-0.15
},
margin=dict(
t=25,
b=125
)
)
return [
html.H6(f'Total Cases: {value}'),
dcc.Graph(figure=fig,config={'displayModeBar': False})
]
保存修改后,刷新网址,此时界面中输出的内容如下。(费时间的地方在于pie图的位置和legend的位置放置,需要慢慢调整参数,达到自己满意的一个样式)
该文件的功能是按照Dropdown组件选取的标签,绘制对应国家/地区的最近30天的疫情确诊数据,以及近七天的平滑数据曲线。首先在api.ipynb文件中获取最近30天的疫情确诊数据,以中国为例。
先提取中国确诊的疫情数据,并借助shift(1)
方法,求解每日新增的确诊数量。
然后借rolling(7)
方法获取数据7日曲线数据。
最后就是对最后30条数据进行切片即可获取对应的数据。把此过程封装为函数get_30_days_data()
,然后保存在api.py文件中,方便调用。
数据都准备完毕后,开始完善barlinechart.py文件。其中go.Figure()
中可以绘制多个图形,图形之间用列表存放;还有重点关于交互图中的hover_text
的设置,此处可以根据自己需要任意设定显示信息,比如这里显示的有日期、数量和国家,扩宽了数据的可视化的角度;此外还有y轴相关信息的设置,同理x轴的设置也是同理,该文件中全部的代码如下。
import dash_bootstrap_components as dbc
from dash import html,Input,Output,dcc
from app import app
from api import get_30_days_data
import plotly.graph_objects as go
def BarLineInfo():
return dbc.Col(id='barline',style={'textAlign':'center','paddingTop':20})
@app.callback(
Output('barline','children'),
[Input('dpd','value')]
)
def update(value):
covid_data_30 = get_30_days_data(value)
fig = go.Figure([
go.Bar(
x = covid_data_30['date'].tail(30),
y = covid_data_30['confirmed new add'].tail(30),
name='Daily Confirmed Cases',
marker = {'color':'orange'},
hoverinfo='text',
hovertext=
'Date: ' + covid_data_30['date'].tail(30).astype(str)+'
' +
'Daily Confirmed Cases:' + [f'{i:.0f}' for i in covid_data_30['confirmed new add'].tail(30)]+'
' +
'Country: ' + covid_data_30['Country/Region'].tail(30).astype(str) + '
'
),
go.Scatter(
x=covid_data_30['date'].tail(30),
y=covid_data_30['Rolling Ave.'].tail(30),
mode='lines',
name='Rolling Average of the last 7 days - Daily Confirmed Cases',
marker={'color': 'pink'},
hoverinfo='text',
hovertext=
'Date: ' + covid_data_30['date'].tail(30).astype(str) + '
' +
'Rolling Ave.:' + [f'{i:.0f}' for i in covid_data_30['Rolling Ave.'].tail(30)] + '
'
)
]
)
fig.update_layout(
legend={
'orientation':'h',
'xanchor':'center',
'yanchor':'bottom',
'x':0.5,
'y':-0.25
},
margin=dict(
t=25,
b=150,
l=0,
r=70
),
yaxis={
'title': 'Daily Confirmed Cases',
'color': 'black',
'showline': True,
'showgrid': True,
'showticklabels': True,
'linecolor': 'white',
'linewidth': 1,
'ticks': 'outside',
'tickfont': {
'family': 'Aerial',
'color': 'white',
'size': 12
}
}
)
return [
html.H6(f'Last 30 Days Confirmed Cases: {value}'),
dcc.Graph(
figure=fig,config={'displayModeBar': False}
)
]
该文件的功能实现各国家地区确诊数据随着时间变化的地图可视化。关于地图可视化,可以参考官方示例代码。由于地理数据是属于面数据(区别于具体的点数据),因此可以采用px.choropleth()
方法绘制。
现在关键点落到地理信息的匹配上,获取的数据中虽然有国家/区域信息,但是和px.choropleth()
方法绘制地图时需要的地理数据不一致,因此需要将目前手上的数据通过某一特定标识进行转化。
px.choropleth()
方法中的location
参数指定的iso_alpha
就可以作为参考,国家/地区的iso编码都是唯一的,因此可以用过地理位置信息。而所有国家的iso编码信息,项目中提供iso编码.txt文件,如下。
从中提取iso编码(对应第一列)、国家/地区名称(英文是倒数第二列,中文是倒数第一列),代码如下。
经过仔细观察后发现,获取的数据库中的疫情数据对应的国家/地区,与iso编码中的国家/地区并不完全匹配。
可以创建一个替换字典,将国家/地区名称化为一致,然后借助replace()
方法进行数据替换。
核实数据是否替换成功。以韩国,朝鲜为例,数据清洗完毕。
接着就是把iso数据和疫情数据进行合并,合并的依据,一个是以Country/Region
字段,一个是以country
字段,合并的代码和输出结果如下
至此把绘制地图可视化的数据准备完毕,接着就是把以上处理数据的代码转移到api.py文件中,并把最终的geo_data
变量导入到mapchart.py文件汇总。 在实际测试的过程中发现针对演示的时间字段也需要进行处理,只提取数据的年月,并把数据转化为字符串格式(原来时间的格式是datetime数据格式)
然后把这两行代码更新到api.py中,然后完善mapchart.py文件,全部代码如下
from dash import html,Input,Output,dcc
from app import app
from api import geo_data
import plotly.express as px
def MapInfo():
#考虑到测试计算机的性能,可以指定数据采样,比如只取十分之一的数据进行演示
geo_data_resample = geo_data.sample(frac = 0.1)
fig = px.choropleth(geo_data_resample.sort_values('date_play'),
locations="iso",
color="confirmed",
hover_name="Country/Region",
animation_frame="date_play",
color_continuous_scale='amp',
height=600)
fig.update_layout(
margin=dict(
l=0,
r=0,
b=0,
t=25,
pad=0
)
)
return dcc.Graph(
figure= fig
)
保存文件后,刷新网址,页面输出结果如下。(受限于测试计算机的性能,使用所有的数据量会导致演示地图稍微卡顿,项目中设置了一个采样方法,这里使用了十分之一的数据量。此外还发现在地图的动态演示中,发现格陵兰岛的数据始终空白)
可以考虑补充这部分数据,假定都是以0进行填充。(这里是实现某一地区数据缺失时的处理方式,如果有真实数据,直接用真实数据按照此方法填补即可),在api.py文件中添加格陵兰岛的数据,并把数据合并到geo_data
变量中,如下
此时保存文件后,刷新网页,动态演示过程中,格陵兰岛的颜色不再是灰白色了,数据值为0。
在assets文件夹下新建一个setting.css文件,设置整体风格。最后一行的overflow-y设置是参照 实战7中3.4 table.py文件完善部分 防止全部全局撑开后网页抖动的设置,具体介绍可以参考实战7。 背景设置为一种蓝色调,字体颜色设置为白色。
setting.py文件中的全部代码如下。(其中只有两个属性是项目9中没有的属性,div_container属性控制四部分的整体布局,card_container属性用于控制组件的样式)
html,body{
background:#0d0247;
color: white;
overflow-y:scroll;}
.div_container{
text-align: center;
margin: 20px;
padding: 20px;
margin-top: 5px;
}
.card_container{
border-radius: 5px;
background-color: #182757;
margin: 16px;
padding: 20px;
position: relative;
box-shadow: 2px 2px 2px #1e294e;
}
/* width */
::-webkit-scrollbar {
width: 10px !important;
display: block !important;
}
/* Track */
::-webkit-scrollbar-track {
background: #232c37 !important;
border-radius: 10px !important;
display: block !important;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #363535 !important;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: white !important;
}
#dpd * {
background-color: #182757 !important;
color: #7f9ab4 !important;
font-size: 10px !important;
}
.Select-control{
border: 3px solid #323844 !important;
}
.Select-menu-outer{
overflow: hidden !important;
border-color: #36425e !important;
}
把div_container属性应用于整个Div,也就是核心四部分的对齐和布局上。
只需要修改一下更新时间的颜色。剩下的一些样式根据实际调试过程中的变化进行修改,比如把00:01(UTC)单独另起一行,然后调整距离右侧的边距,使之与上面的日期文字呈现中间对齐的样子。
在Col容器中应用卡片属性card_container
,并根据实际显示调整width的值,原来是3,现在调整为2.5。
关于绘制的图形中白色背景去除的问题,只需要添加两行代码即可(其余图形中的背景去除也是相同方式)。#182757就是卡片属性中的背景颜色。
和前面的修改一致。
但是不知怎么最后一个地图的部分总是显示左右无法和前面部分的组件对齐,因此在index.py中单独设置一下左右边距,实测左侧少了15px,右侧多了15px,如下
至此,整个项目就梳理完毕,撒花✿✿ヽ(°▽°)ノ✿