Plotly與Dash介紹
Plotly是近年Python互動資料視覺化非常強大的模組,可以用Python語法控制D3.js生成多樣化圖表,支援最潮的ML資料科學也沒問題。擁有完善的widget可做資料互動。結合相關套件Dash後可將圖表輕鬆鑲嵌到Django和其他Python後端模組。
Dash 是建構於 Plotly.js、React.js 與 Flask 之上的 Python 網頁應用程式框架。等同於用Python來完成前端框架React部分功能,不用另外寫JS就能創造出互動性高的動態圖表,只要會簡單的html就能用Plotly加Dash串連前後端生成漂亮的圖表網頁。若想在Jupyter上Run Dash,可另外加裝jupyter-dash模組。
中文世界學習Plotly&Dash的資源不多,這系列會記錄一下這兩個套件的使用重點,範例以製作投資方面的圖表與儀表板為主。
範例目標
若今天想要依照日期查每日已實現損益的變化、標的損益佔比分佈、賺賠比,在券商app的對帳單無法實現,能不能客製化一個dashboard,只要傳入對帳單就能生成圖表呢?先看一下生成結果的靜態圖。以下會講幾個程式重點,文末會有colab連結供各位參考。
程式重點
整理對帳單格式
# 欄位修改:'日期':'date','損益':'pnl',日期改西元年datetime格式,stock_id換成股票代碼加中文名稱
def process_data(path='drive/MyDrive/sk88/2021對帳.csv'):
df = pd.read_csv(path)
df = df.rename(columns={'日期': 'date', '損益': 'pnl'})
df = df[df['date'] != '總計:']
df['date'] = df['date'].apply(
lambda s: datetime.strptime(str(int(s[:s.index('/')]) + 1911) + s[s.index('/'):], "%Y/%m/%d"))
df['stock_id'] = df['股票'] + ' ' + df['股名']
return df
每家券商對帳單格式不同,於process_data()修改欄位,自行複寫function。
你可以選擇寫爬蟲、連接api(不是每家都有)從券商拿對帳單,或是自行手動下載對帳單再丟入google drive,範例是使用手動的方式。
demo使用新光證券富貴角7號下載的對帳單,stock_id、date、pnl(已實現損益)是必要欄位,date換成西元年格式,之後dash的DatePickerRange會用到日期查詢互動的部分。colab裡的demo_df為dataframe範例格式供參考。
繪圖物件
RealizedProfitLoss物件結構主要有3個function,初始化實體屬性為丟入df生成圖表。
class RealizedProfitLoss:
def __init__(self,df):
self.dataframe=df
def plot(self,start_date=None, end_date=None):
def run_dash(self):
Plot function
def plot(self,start_date=None, end_date=None):
# 日期控制
df=self.dataframe
if start_date:
df = df[df['date'] >= start_date]
if end_date:
df = df[df['date'] <= end_date]
# 依照stock_id 分group計算每個標的的損益
date_group = df.groupby(['date'])[['pnl']].sum()
df = df.groupby(['stock_id'])[['pnl']].sum()
df = df.reset_index()
df = df.sort_values(['pnl'])
# 分類賺賠
df['category'] = ['profit' if i > 0 else 'loss' for i in df['pnl'].values]
df['pnl_absolute_value'] = abs(df['pnl'])
# 製作Sunburst賺賠合併太陽圖所需資料
df_category = df.groupby(['category'])[['pnl_absolute_value']].sum()
df_category = df_category.reset_index()
df_category = df_category.rename(columns={'category': 'stock_id'})
df_category['category'] = 'total'
df_total = pd.DataFrame(
{'stock_id': 'total', 'pnl_absolute_value': df['pnl_absolute_value'].sum(), 'category': ''},
index=[0])
df_all = pd.concat([df, df_category, df_total])
labels = df['stock_id']
# Create subplots: use 'domain' type for Pie subplot
fig = make_subplots(rows=4,
cols=3,
specs=[[{'type': 'domain', "rowspan": 2}, {'type': 'domain', "rowspan": 2},{'type': 'domain', "rowspan": 2}],
[None, None, None],
[{'type': 'xy', "colspan": 3, "secondary_y": True}, None, None],
[{'type': 'xy', "colspan": 3}, None, None]],
horizontal_spacing=0.03,
vertical_spacing=0.08,
subplot_titles=('Profit Pie: ' + str(df[df['pnl'] > 0]['pnl'].sum()),
'Loss Pie: ' + str(df[df['pnl'] < 0]['pnl'].sum()),
'Profit Loss Sunburst: '+str(df['pnl'].sum()),
'Profit Loss Bar By Date',
'Profit Loss Bar By Target',
)
)
# 獲利donut圖
fig.add_trace(go.Pie(labels=labels, values=df['pnl'], name="profit", hole=.3, textposition='inside',
textinfo='percent+label'), row=1, col=1)
# 虧損donut圖
fig.add_trace(go.Pie(labels=labels, values=df[df['pnl'] < 0]['pnl'] * -1, name="loss", hole=.3,
textposition='inside', textinfo='percent+label'), row=1, col=2)
# 賺賠合併太陽圖
fig.add_trace(go.Sunburst(
labels=df_all.stock_id,
parents=df_all.category,
values=df_all.pnl_absolute_value,
branchvalues='total',
marker=dict(
colors=df_all.pnl_absolute_value.apply(lambda s: math.log(s + 0.1)),
colorscale='earth'),
textinfo='label+percent entry',
), row=1, col=3)
# 每日已實現損益變化
fig.add_trace(go.Bar(x=date_group.index, y=date_group['pnl'], name="date", marker_color="#636EFA"), row=3, col=1)
fig.add_trace(
go.Scatter(x=date_group.index, y=date_group['pnl'].cumsum(), name="cumsum_realized_pnl",
marker_color="#FFA15A"),
secondary_y=True,row=3, col=1)
# 標的損益變化
fig.add_trace(go.Bar(x=df['stock_id'], y=df['pnl'], name="stock_id", marker_color="#636EFA"), row=4, col=1)
# 修正Y軸標籤
fig['layout']['yaxis']['title'] = '$NTD'
fig['layout']['yaxis2']['showgrid']=False
fig['layout']['yaxis2']['title'] = '$NTD(cumsum)'
fig['layout']['yaxis3']['title'] = '$NTD'
# 主圖格式設定標題,長寬
fig.update_layout(
title={
'text': f"Realized Profit Loss Statistic ({start_date}~{end_date})",
'x': 0.49,
'y': 0.99,
'xanchor': 'center',
'yanchor': 'top'},
width=1200,
height=1000)
return fig
plotly.express是用在比較不需要客製化圖表的情況,高級封裝語法,短短一行便可生成精美圖表,但在本次範例是以plotly.graph_objects操作,graph_objects是plotly下的底層圖形物件,plotly.express背後也是基於graph_objects實作,可以做客製化的設定。以下將幾個會用到的圖表官方教學連結附上,多是參考官方範例做調整
1.make_subplots :製作子圖畫結構圖。
rows設定子圖列數,cols設定子圖欄數。
specs設定每個子圖的格式,注意每個圖表使用的type不一樣,例如’domain’ 是用在 Pie,預設是xy,適用座標圖。colspan和rawspan可以做子圖佔比控制,和html語法一樣。若子圖有雙Y軸,記得設定”secondary_y”: True。
subplot_titles可控制子圖標題,這邊加上賺賠與損益總金額到子圖的標題上。
2.fig.add_trace:控制子圖資訊,每一個子圖物件都要用fig.add_trace把字圖加入畫布結構中。使用row與col設定子圖在make_subplots中specs的相對位置。
3.pie-charts and sunburst:sunburst可以做整合性的多層圓餅圖資料呈現。製作虧損標的的分佈圖時,注意要將值轉成絕對值,圖表顯示不出負數出來。hole參數控制中心洞的比例。textposition控制顯示資訊的位置,範例設定在圖形內。textinfo控制顯示的資訊,一般圓餅圖會使用’percent+label’顯示百分比和每一項資料的名稱。
太陽圖的marker可以做複雜的顏色設定,此範例各項資料的顏色會依照個比例數值帶進colorscale做漸層變化。
4.bar :棒狀圖,繪製每日已實現損益變化和標的損益排序。marker_color調整柱狀顏色。
5.fig dict與fig.update_layout:fig是dict的形式,可以直接透過修改dict裡的value調整圖型格式,或是用update_layout的function來修改。若子圖物件沒有參數可調整,可進來fig修正,此範例是修改Y軸標籤,我們有兩個設有雙Y軸的棒狀圖,所以可針對yaxis~yaxis3共四個yaxis做設定,’title’設定標籤名,’showgrid’控制該軸是否產生格線,通常會只用一鞭產生格線,不然圖表太雜亂。
Run Dash
def run_dash(self):
# Build App
app = JupyterDash(__name__)
app.layout = html.Div([
html.H1("Realized Profit Loss JupyterDash"),
html.P("date_range:"),
dcc.DatePickerRange(
id='my-date-picker-range',
min_date_allowed=date(1990, 1, 1),
max_date_allowed=date(2100, 12, 31),
initial_visible_month=date(2021, 1, 1),
start_date=date(2021, 1, 1),
end_date=date(2021, 12, 31)
),
dcc.Graph(id="graph")
])
@app.callback(
Output("graph", "figure"),
[Input('my-date-picker-range', 'start_date'),
Input('my-date-picker-range', 'end_date')])
def update_output(start_date, end_date):
return self.plot(start_date, end_date)
# Run app and display result inline in the notebook
app.run_server(mode='inline')
app = JupyterDash(__name__)讓dash程式在jupyter上運行,colab無法開80port網頁出來跑。
app.layout鑲嵌入html語法,製作我們dashboard的網頁頁面。
DatePickerRange是來自dash_core_components(dcc)的日期選取器,可以生成一個widget讓我們選取要查詢的對帳單範圍,裡頭參數可設定預設顯示日期、極限範圍等等,id會關聯到將日期input到繪圖程式的功能。
dcc.Graph(id=”graph”)可嵌入plotly圖表到layout。
@app.callback設定output和input物件,參數帶id和變數名稱,如Output(“graph”, “figure”),在此範例,output是圖表,input是對帳單起始日期。
update_output(start_date, end_date)這個func會帶入前面設定的Input變數start_date, end_date回傳入繪圖程式plot(),做update的動作。
app.run_server(mode=’inline’),記得設mode=’inline’,才能在jupyter上顯示dash。
之後執行程式,loading跑完之後,美美的互動圖表就生成啦!右側的label列可以點掉標的,圖表就會扣除該標的重新計算比例,右上有plotly自帶的工具列,有縮放、截圖、拖拉等好用功能。