财险数据交互式可视化——运用Python的Bokeh包
-
导引
继 Alonso 上篇用Python分析财险数据——菜鸟向,我对同样的数据用 Bokeh server 进行了可视化。
Bokeh简单介绍:Bokeh 是 Python 的一个制作交互式可视化工具的包,R 中也有相应的包叫做 shiny (https://shiny.rstudio.com/)。Boken 目前对中文的支持不太友好,但本文我们将用 JS 将网页语言改变为中文。Boken 有两种用法:
- 第一种是不利用 Bokeh server,这种情况下能做出好看的交互图,实现拖曳,放大,鼠标悬浮标签等功能。最后能够生成静态的HTML文件。
- 第二种是利用 Bokeh server,做一个 Web application。这种情况下能实现数据筛选调用等更多功能。一般使用 Flask + Bokeh,把 Bokeh 放置于 Flask application 里面。我们的这个例子中没有使用 FLask,而用了一个默认的 HTML 模板,叫做 Jinja,很多可以修改的功能被限制了。
本文介绍的是第二种,Bokeh server 的 Web application 应用示例,代码基于 Bokeh Gallery 里面的两个 sample。一个是 movie,一个是 crossfilter,链接见文末的参考文献。
先来示范一下效果:
- 筛选数据功能:
- 拖曳,选择,数据标签功能:
- 通过拖曳点的方式修改数据的功能:
该交互式图表目前 host 于http://49.234.103.189:5006/test 这个网页中。
步骤
安装 Bokeh
pure python 用户打开命令行:
pip install bokeh
conda 用户:
conda install bokeh
文件树
我们需要的文件树大概是这样的结构:
app 文件夹下有三个文件:一个是 main.py,是我们的 python 主文件;另一个是 templates 文件夹,里面放 index.html,是我们对于基本 html 框架的补充;还有一个是 lidata.csv,是我们的数据源文件。
分析数据
我们要根据公司,险种,险别来进行数据筛选,因此,我们首先要得到这几列有哪些情况。
# lidata就是Alonso的数据集 df_all = pd.read_csv(r'./app/data/lidata.CSV', header = 0) # 计算ULR df_all['ULR'] = df_all['UL'] / df_all['EP'] # 加入all是为了能够选择所有情况 unique_company = ["All"] + df_all['公司'].unique().tolist() unique_business = ["All"] + df_all['险种'].unique().tolist() unique_product = ["All"] + df_all['险别'].unique().tolist()
需要对不同险别展示不同颜色,代码如下
color = pl.mpl['Plasma'][len(unique_product)] #这里Plasma是一个Bokeh自带的调色盘,帮助我们找到好看的配色 df_all["color"] = [color[unique_product.index(pro)] for pro in df_all["险别"].values]
我们还要筛选展示的事故年,因此,我们需要读取最小的事故年和最大的事故年。
year_start = df_all['事故年'].min() year_end = df_all['事故年'].max()
最后一个要准备的是要展示的数据y列是什么。这里需要做一个字典用来对应选项和数据列名的关系。
axis_map = { "ULR": "ULR", "ULAE": "EP", "DAC":'DAC' }
接下来就是作图啦。图分为左右两边。左边的部分叫做 control,右边的部分叫做 plot。
制作control
# year_range: 展示的事故年范围 year_range = RangeSlider(start=year_start, end=year_end, value=(year_start,year_end), step=1, title="展示年") Slider(title="开始展示年", start=year_start, end=year_end, value=year_start, step=1) max_year = Slider(title="结束展示年", start=year_start, end=year_end, value=year_end, step=1) # 选择的公司,险别,险种 company = Select(title="公司选择", value="All", options=unique_company) business = Select(title="险别选择", value="All", options=unique_business) product = Select(title="险种选择", value="All", options=unique_product) y_axis = Select(title="展示值", options=sorted(axis_map.keys()), value="ULR") controls = [company, business, product, year_range, y_axis]
制作plot
# Tooltips用来制作鼠标悬浮于数据时的数据标签 TOOLTIPS=[ ("公司为", "@com"), ("年:", "@year"), ("险别为", "@business"), ("险种为", "@pro") ] # TOOLS规定了哪些工具要显示出来,比如拖曳等 TOOLS="pan,wheel_zoom,box_select,lasso_select,reset" p = figure(tools=TOOLS,plot_height=100, plot_width=200, title="", toolbar_location="above", tooltips=TOOLTIPS, sizing_mode="scale_both") r = p.circle(x="x",y="y" ,source=source, size=10, color = 'color', alpha=0.6, hover_color='white', hover_alpha=0.5) # PointDrawTool这个工具需要单独放入其中 draw_tool = PointDrawTool(renderers=[r], empty_value='black') p.add_tools(draw_tool) p.toolbar.active_tap = draw_tool
更新数据
def select_products(): # strip可以去除数据前面或者后面的空格 company_val = company.value.strip() business_val = business.value.strip() product_val = product.value.strip() # 选择事故年 selected = df_all[ (df_all.事故年 >= year_range.value[0]) & (df_all.事故年 <= year_range.value[1]) ] # 选择公司,险种,险别等 if (company_val != "All"): selected = selected[selected.公司.str.contains(company_val)==True] if (business_val != "All"): selected = selected[selected.险种.str.contains(business_val)==True] if (product_val != "All"): selected = selected[selected.险别.str.contains(product_val)==True] return selected # 这个函数用来更新数据源 def update(): df = select_products() x_name = "事故年" y_name = axis_map[y_axis.value] p.title.text = "%d points selected" % len(df) source.data = dict( x=df[x_name], y=df[y_name], com=df["公司"].values, year=df["事故年"].values, business=df["险别"].values, pro=df["险种"].values, color = df["color"] ) # control中的每一个元素改变后,都需要运行update() for control in controls: control.on_change('value', lambda attr, old, new: update())
生成图
# input就是图左边的control inputs = column(*controls, width=320, height=1000) inputs.sizing_mode = "fixed" l = layout([ [inputs, p], ], sizing_mode="scale_both") update() curdoc().add_root(l, p)
templates文件夹:利用Bokeh自带Jinja模板对网页更改基本样式
这个时候就要用到templates文件夹啦!它里面的index.html是对于jinja模板的补充。
Jinja模板如下,这个我们没有办法改,想要改的话只能用JS在后面改。
<!DOCTYPE html> <html lang="en"> {% block head %} <head> {% block inner_head %} <meta charset="utf-8"> <title>{% block title %}{{ title | e if title else "Bokeh Plot" }}{% endblock %}</title> {% block preamble %}{% endblock %} {% block resources %} {% block css_resources %} {{ bokeh_css | indent(8) if bokeh_css }} {% endblock %} {% block js_resources %} {{ bokeh_js | indent(8) if bokeh_js }} {% endblock %} {% endblock %} {% block postamble %}{% endblock %} {% endblock %} </head> {% endblock %} {% block body %} <body> {% block inner_body %} {% block contents %} {% for doc in docs %} {{ embed(doc) if doc.elementid }} {% for root in doc.roots %} {{ embed(root) | indent(10) }} {% endfor %} {% endfor %} {% endblock %} {{ plot_script | indent(8) }} {% endblock %} </body> {% endblock %} </html>
index.html 的基本格式如下:
{% extends base %} <!-- goes in head --> {% block preamble %} <link href="app/static/css/custom.min.css" rel="stylesheet"> {% endblock %} <!-- goes in body --> {% block contents %} <div> {{ embed(roots.scatter) }} </div> <div> {{ embed(roots.line) }} </div> {% endblock %}
我在标准模板中加了一些代码,来保证 html 的语言选项是 zh,也就是中文,加以对CSS文件的修改,就大功告成啦!
window.onload = function() { document.querySelector("html").lang = "zh"; };
运行
在命令行中先 cd 到 app 所在文件夹,并输入:
bokeh serve app
或者
bokeh serve --show app
或在 Debug 模式运行
bokeh serve --log-level=debug app
当 python 由于版本不同可能有冲突时,可以使用:
python3 -m bokeh serve app
完整代码在github: https://github.com/Mengkee/bokeh_example
参考文献