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的編程教室
這裏還有更多精彩。一起學,走得遠