1. 需要
做股票剖析的敌人常常会见到相似这种的期货公司持仓榜单图:
这种图就是棒棒糖图。也就是咱们明天文章的指标:
绘制出期货持仓榜单的棒棒糖图
图中线的两端是圆点或者菱形,旁边都有标注持仓证券商和绝对应的持多仓数或持空仓数,且左右线色彩不同。画图思路大体就是:先画程度线图,再用 scatter 散点图画线左右两端的点,而后标注两端名称,以及题目和注解。
Python 中比拟罕用的两种图表库是 matplotlib 和 plotly。上图就是以 matplotlib 绘制。而 Plotly 交互性更好。
更进一步,如果想让用户能够点击抉择交易日期,查看该日期对应的榜单图,这就能够通过一个响应式 web 应用程序来实现。Dash 是一个基于 python 的交互式可视化 web 利用框架,matplotlib 和 Plotly 都可与 Dash 框架联合应用。
Matplotlib 大家比拟相熟。在开始之前,咱们先简略介绍下 plotly 和 Dash。
2. Plotly
plotly 库(plotly.py)是一个交互式的开源绘图库,反对 40 多种独特的图表类型,涵盖各种统计,财务,天文,迷信和三维用例,是实用于 Python,R 和 JavaScript 的交互式图表库。
plotly.py 建设在 Plotly JavaScript 库(plotly.js)之上,使 Python 用户能够创立基于 Web 的丑陋交互式可视化成果。这些可视化成果能够显示在 Jupyter 笔记本中,能够保留到独立的 HTML 文件中,也能够作为纯 Python 应用。其官网文档上提供了各种图标的接口阐明。
3. Dash
Dash 是用于构建 Web 应用程序的 Python 框架。Dash 建设在 Flask、Plotly.js 和 React.js 根底之上,即 Dash 中的控件和其触发事件都是用 React.js 包装的,Plotly.js 为 Dash 提供弱小的交互式数据可视化图库,Flask 为其提供后端框架。这个框架对 python 程序员特地敌对,只须要写 python 代码,不须要写 JS 代码,间接拖拽控件来用即可。感兴趣的童鞋能够去 Dash 的官网文档多多理解一下。Dash 是应用纯 Python 构建高度自定义用户界面的数据可视化应用程序的现实抉择。它特地适宜做数据分析、数据可视化以及仪表盘或者报告展现。能够将 Dash 应用程序部署到服务器,而后通过 URL 共享它们,不受平台和环境的限度。
4. 装置
在画图之前,咱们须要装一下 Dash、plotly 相干包。能够用 pip 装:
pip install plotly dash
或者也能够用 conda 进行装置。
5. 数据格式和数据处理
测试数据来自西方财富网,用 csv 文件格式保留。
数据的格局如下,header 是日期为第一列,第 3 列往后为期货公司名字。表格中的正数是下面图中蓝色的空仓,负数是红色的多仓。绘图时,从表格中取出某一日期的一行记录,将持仓数目排序,把对应的数据存入列表中,之后进行画图。
首先对数据进行荡涤和解决,pandas 读取数据,这里须要去除 000905_SH 列,以及删除全 0 行。代码如下:
excel_pd = pd.read_excel('data/IC 期货商历史数据(1).xlsx', index_col='日期')
# 去空
excel_pd.dropna()
# 去除 000905_SH 列
excel_pd = excel_pd.drop(labels='000905_SH', axis=1)
# 去 0 行
excel_pd = excel_pd[~(excel_pd == 0).all(axis=1)]
# 取出工夫列表,获取最大日期和最小日期,为日历选项做判断
date_list = excel_pd.index.values.tolist()
min_date = min(date_list)
max_date = max(date_list)
接下来咱们须要依据输出的日期来筛选某一行记录,别离将持空仓期货公司和持多仓期货公司取出,剔除持仓数为 0 的期货公司。代码如下:
def get_data_via_date_from_excel(date):
# 筛选日期
sheet1_data = excel_pd.loc[date]
# 去除列值为 0
sheet1_data = sheet1_data[sheet1_data != 0]
# 排序 从小到大
sheet1_data = sheet1_data.sort_values()
# 空仓
short_hold = sheet1_data[sheet1_data < 0]
# 多仓
long_hold = sheet1_data[sheet1_data >= 0].sort_values(ascending=False)
return short_hold, long_hold
6. 画图
Matplotlib 画图
创立一张画布 figure 和 ax 画图层,用 ax.hlines 别离画空仓水平线和多仓水平线。用 ax.scatter 画左右两边线的散点,应用菱形 marker。应用 plt.text 别离画线两端的标注期货公司和持仓数。plt.annotate 画排名标注,别离设置色彩和字体大小。
但这个成果是反的,咱们是心愿排名最后面的在上,排名最初面的下。这时咱们能够设置 y 轴反置一下 ax.invert_yaxis()。增加图例和题目以及设置坐标轴不可见,失去最终成果:
外围代码如下:
def draw_lollipop_graph(short_hold, long_hold, date):
# sheet_major.index.values.tolist()
fig, ax = plt.subplots(figsize=(10, 8))
# 空仓水平线
ax.hlines(y=[i for i in range(len(short_hold))], xmin=list(short_hold), xmax=[0] * len(short_hold.index), color='#1a68cc', label='空')
# 多仓水平线
ax.hlines(y=[i for i in range(len(long_hold))], xmax=list(long_hold), xmin=[0] * len(long_hold.index), color='red', label='多')
# 画散点
ax.scatter(x=list(short_hold), y=[i for i in range(len(short_hold))], s=10, marker='d', edgecolors="#1a68cc", zorder=2, color='white') # zorder 设置该点笼罩线
ax.scatter(x=list(long_hold), y=[i for i in range(len(long_hold))], s=10, marker='d', edgecolors="red", zorder=2, color='white') # zorder 设置该点笼罩线
# 画线两端标注图
for x, y, label in zip(list(short_hold), range(len(short_hold)), short_hold.index):
plt.text(x=x, y=y, s=label+'({})'.format(abs(x)), horizontalalignment='right', verticalalignment='center', fontsize=10)
for x, y, label in zip(list(long_hold), range(len(long_hold)), long_hold.index):
plt.text(x=x, y=y, s=''+label+'({})'.format(abs(x)), horizontalalignment='left', verticalalignment='center', fontsize=10)
# 设置排名
size = [17, 16, 15] + [8 for i in range(max(len(short_hold), len(long_hold))-3)]
color = ['#b91818', '#e26012', '#dd9f10'] + ['#404040' for i in range(max(len(short_hold), len(long_hold))-3)]
for i, s, c in zip(range(max(len(short_hold), len(long_hold))+1), size, color):
plt.annotate(s=i+1, xy=(0, i), fontsize=s, ma='center', ha='center', color=c)
# 坐标轴 y 反置
ax.invert_yaxis()
# 坐标轴不可见
ax.set_xticks([])
ax.set_yticks([])
ax.spines['top'].set_visible(False) # 去上边框
ax.spines['bottom'].set_visible(False) # 去下边框
ax.spines['left'].set_visible(False) # 去左边框
ax.spines['right'].set_visible(False) # 去左边框
# 设置 title
ax.set_title('黄金持仓龙虎榜单({})'.format(date), position=(0.7, 1.07), fontdict=dict(fontsize=20, color='black'))
# 主动获取 ax 图例句柄及其标签
handles, labels = ax.get_legend_handles_labels()
plt.legend(handles=handles, ncol=2, bbox_to_anchor=(0.75, 1.05), labels=labels, edgecolor='white', fontsize=10)
# 保留 fig
image_filename = "lollipop_rank.png"
plt.savefig(image_filename)
encoded_image = base64.b64encode(open(image_filename, 'rb').read())
# plt.show()
return encoded_image
Plotly 画图
1) Figure 是一张画布,跟 matplotlib 的 figure 是一样,数据是字典模式,创立代码如下:
import plotly.graph_objects as go
fig = go.Figure() # 创立空画布
fig.show()
2) Traces 轨迹,即所有的图表层都是在这里画的,轨迹的类型都是由 type 指定的(例如 ”bar”,”scatter”,”contour” 等等)。轨迹是列表,创立代码如下:
fig = go.Figure(data=[trace1, trace2]) # 定义 figure 时加上轨迹数据
Figure.add_traces(data[, rows, cols, …]) # 或者先定义一张空的画布,再增加轨迹
Figure.update_traces([patch, selector, row, …]) # 更新轨迹
# 可运行代码
import plotly.graph_objects as go
trace = [go.Scatter( # 创立 trace
x=[0, 1, 2],
y=[2, 2, 2],
mode="markers",
marker=dict(color="#1a68cc", size=20),
)]
fig = go.Figure(data=trace)
fig.show()
3) Layout 层,设置题目,排版,页边距,轴,正文,形态,legend 图例等等。布局配置选项实用于整个图形。
import plotly.graph_objects as go
trace = [go.Scatter(x=[0, 1, 2],
y=[2, 2, 2],
mode="markers",
marker=dict(color="#1a68cc", size=20),
)]
# 创立 layout,增加题目
layout = go.Layout(title=go.layout.Title(text="Converting Graph Objects To Dictionaries and JSON")
)
fig = go.Figure(data=trace, layout=layout)
fig.show()
Figure.update_layout([dict1, overwrite]) # 也可应用 API 更新图层
4) Frames 帧幅轨迹,是在 Animate 中用到的渲染层,即每多少帧幅动画遍历的轨迹,与 traces 很像,都是列表模式,应用时须要在 layout 的 updatemenus 设置帧幅距离等等。具体用法能够去看看官网文档用法,比较复杂,这里不过多介绍。上面回归正题,咱们须要创立一张画布 figure 来画图。
画图 1:水平线
因为 plotly 没有 matplotlib 的 ax.hlines 函数画水平线,能够借助 plotly shapes 画水平线。画 shapes 图须要晓得该点坐标 (x1,y1) 还要找到对应的 (0,y1) 坐标点并连线组成一个 shape,这里 x1 是持仓数,y1 就用持仓列表的下标示意。
# 空仓水平线
short_shapes = [{'type': 'line',
'yref': 'y1',
'y0': k,
'y1': k,
'xref': 'x1',
'x0': 0,
'x1': i,
'layer': 'below',
'line': dict(color="#1a68cc",),
} for i, k in zip(short_hold, range(len(short_hold)))]
# 多仓水平线
long_shapes = [{'type': 'line',
'yref': 'y1',
'y0': k,
'y1': k,
'xref': 'x1',
'x0': j,
'x1': 0,
'layer': 'below',
'line': dict(color="red",)
} for j, k in zip(long_hold, range(len(long_hold)))]
画图 2:线两端散点和标注
用 scatter 画左右两边线的散点,应用菱形 marker 并且 scatter 中的 text 能够标注线两端的标注期货公司和持仓数,留神持仓数都是负数。
# 画散点
fig.add_trace(go.Scatter(
x=short_hold,
y=[i for i in range(len(short_hold))],
mode='markers+text',
marker=dict(color="#1a68cc", symbol='diamond-open'),
text=[label + '(' + str(abs(i)) + ')' for label, i in zip(short_hold.index, short_hold)], # 散点两端的期货公司标注和持仓数
textposition='middle left', # 标注文字的地位
showlegend=False # 该轨迹不显示图例 legend
))
fig.add_trace(go.Scatter(
x=long_hold,
y=[i for i in range(len(long_hold))],
mode='markers+text',
text=['' + label +'('+ str(abs(i)) +')' for label, i in zip(long_hold.index, long_hold)], # 散点两端的期货公司标注和持仓数
marker=dict(color='red', symbol='diamond-open'),
textposition='middle right', # 标注文字的地位
showlegend=False # 该轨迹不显示图例 legend
))
画图 3:排名标注
持续用 scatter 只显示 text 来画排名标注,别离设置色彩和字体大小。
# 线上的排名顺序
fig.add_trace(go.Scatter(x=[0]*max(len(short_hold), len(long_hold)),
y=[i for i in range(max(len(short_hold), len(long_hold)))],
mode='text',
text=[str(i+1) for i in range(max(len(short_hold), len(long_hold)))], # 排名从 1 开始
textfont=dict(color=['#b91818', '#e26012', '#dd9f10'] + ['#404040' for i in range(max(len(short_hold), len(long_hold)) - 3)],
size=[17, 16, 15] + [10 for i in range(max(len(short_hold), len(long_hold)) - 3)],
family="Open Sans"),
textposition='top center',
showlegend=False
))
画图 4:图例
因为 plotly shapes 不是轨迹,只是 layout 中的一部分,所以不能增加 legend,而下面的散点 scatter 虽是轨迹,然而 mode =markers+text 使得 legend 中多出了 text 文字,如下图,而且目前版本的 plotly 不具备自定义 legend 去除 text 性能。
所以咱们须要本人增加 2 条轨迹来显示 legend 图例,代码如下:
# 加上这条 trace 只是为了显示 legend 图例, 因为 scatter 图例中显示的 text 在 plotly 现有的版本根底上去除不了
fig.add_trace(go.Scatter(x=[0, long_hold[0]],
y=[range(len(long_hold))[0], range(len(long_hold))[0]],
mode='lines',
marker=dict(color='red'),
name='多'
))
fig.add_trace(go.Scatter(x=[0, short_hold[0]],
y=[range(len(short_hold))[0], range(len(short_hold))[0]],
mode='lines',
marker=dict(color='#1a68cc'),
name='空'
))
设置 y 轴反置 autorange=’reversed’ 可让排名最后面的在上,排名最初面的在下,之后设置图里地位,增加题目以及设置坐标轴不可见,代码如下:
# X, Y 坐标轴不可见
fig.update_xaxes(
showticklabels=False,
showgrid=False,
zeroline=False,)
fig.update_yaxes(
showticklabels=False,
showgrid=False,
zeroline=False,
autorange='reversed' # Y 轴倒置)
fig.update_layout(
shapes=short_shapes+long_shapes, # 增加水平线
width=2100,
height=900,
legend=dict(x=0.62, y=1.02, orientation='h'),
template="plotly_white",
title=dict(text='黄金持仓龙虎榜单(' + date + ')',
y=0.95,
x=0.65,
xanchor='center',
yanchor='top',
font=dict(family="Open Sans", size=30)
)
)
7. 创立 Dash 应用程序
这里首先创立一个 Dash app 程序。Dash 应用程序由两局部组成。第一局部是应用程序的“布局”,它形容了应用程序的外观,即应用的 web 界面控件和 CSS 等,dash_core_components 和 dash_html_components 库中提供一组用 react.js 包装好的组件,当然相熟 JavaScript 和 React.js 也可构建本人的组件。第二局部形容了应用程序的交互性,即触发 callback 函数实现数据交互。
这里咱们须要调用 Dash 中的日历控件 dcc.DatePickerSingle,具体用法能够参考官网文档,还有一个能够搁置 dcc.Graph 图的容器 html.Div()。同时通过 callback 函数来捕获日期更新从而画图事件。
import dash
import dash_html_components as html
import dash_core_components as dcc
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.layout = html.Div([
html.Div(dcc.DatePickerSingle(
id='my-date-picker-single',
min_date_allowed=min_date, # 日历最小日期
max_date_allowed=max_date, # 日历最大日期
date=max_date # dash 程序初始化日历的默认值日期
), style={"margin-left": "300px"}),
html.Div(id='output-container-date-picker-single', style={"text-align": "center"})
])
Matplotlib + Dash 框架
之前咱们用 matplotlib 画好的榜单图曾经编码保留好,留神这里画的图是动态图,触发日期更新画 matplotlib 画图事件代码如下:
@app.callback(Output('output-container-date-picker-single', 'children'),
[Input('my-date-picker-single', 'date')])
def update_output(date):
print("date", date)
if date is not None:
if date not in date_list:
return html.Div(["数据不存在"])
encoded_image = create_figure(date)
return html.Div([html.Img(src='data:image/png;base64,{}'.format(encoded_image.decode()), style={"text-align": "center"})
])
if __name__ == '__main__':
app.run_server(debug=True)
启动应用程序,在浏览器中输出控制台的如下地址和端口号拜访该网页:
@app.callback(Output('output-container-date-picker-single', 'children'),
[Input('my-date-picker-single', 'date')])
def update_output(date):
print("date", date)
if date is not None:
if date not in date_list:
return html.Div(["数据不存在"])
fig = create_figure(date)
return html.Div([dcc.Graph(figure=fig)
])
if __name__ == '__main__':
app.run_server(debug=True) # 启动应用程序
Plotly + Dash 框架
Plotly 画图的函数中返回的 fig 能够间接搁置在 Dash 组件库中的 Dcc.Graph 中, Dash 是 plotly 上面的一个产品,外面的画图组件库简直都是 plotly 提供的接口,所以 plotly 画出的交互式图能够间接在 Dash 中展现,无需转换。触发日期更新 plotly 画图事件代码如下:
按之前同样形式启动应用程序,在浏览器中拜访网页。
8. 结语
Matlplotlib 库弱小,性能多且全,然而呈现的图都是动态图,不便于交互式,对有些简单的图来说可能其库外面用法也比较复杂。对于这个榜单图来说可能 matplotlib 画图更不便一些。
Plotly 库是交互式图表库,图形的品种也多,画出的图比拟炫酷,鼠标点击以及悬停能够看到更多的数据信息,还有各种气泡图,滑动 slider 动画效果图,且生成的图片保留在 html 文件中,虽说有些性能比不上 matplotlib 全而弱小,像这个榜单图,没有水平线 hline 或竖直线 vline,虽有 shape,但不能为 shapes 增加图例,然而这个库也在缓缓倒退,官方论坛 community 外面也有许多人提出问题,各路大神也都在解决这些问题,置信之后 plotly 的性能会越来越全。
文中代码及数据已上传,地址: https://pan.baidu.com/s/1Uv_cursTr0cbTLB3D6ylIw 提取码: jmch
—-
获取更多教程和案例,
欢送搜寻及关注:Crossin 的编程教室
这里还有更多精彩。一起学,走得远