Plotly&Dash初體驗|已實現損益儀表板|DashBoard製作教學(1)

  • Post author:
  • Reading time:8 mins read

Plotly與Dash介紹

Plotly是近年Python互動資料視覺化非常強大的模組,可以用Python語法控制D3.js生成多樣化圖表,支援最潮的ML資料科學也沒問題。擁有完善的widget可做資料互動。結合相關套件Dash後可將圖表輕鬆鑲嵌到Django和其他Python後端模組。

截圖 2021 05 03 下午7.33.54

Dash 是建構於 Plotly.js、React.js 與 Flask 之上的 Python 網頁應用程式框架。等同於用Python來完成前端框架React部分功能,不用另外寫JS就能創造出互動性高的動態圖表,只要會簡單的html就能用Plotly加Dash串連前後端生成漂亮的圖表網頁。若想在Jupyter上Run Dash,可另外加裝jupyter-dash模組。

中文世界學習Plotly&Dash的資源不多,這系列會記錄一下這兩個套件的使用重點,範例以製作投資方面的圖表與儀表板為主。

範例目標

若今天想要依照日期查每日已實現損益的變化、標的損益佔比分佈、賺賠比,在券商app的對帳單無法實現,能不能客製化一個dashboard,只要傳入對帳單就能生成圖表呢?先看一下生成結果的靜態圖。以下會講幾個程式重點,文末會有colab連結供各位參考。

newplot 12

程式重點

整理對帳單格式


# 欄位修改:'日期':'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自帶的工具列,有縮放、截圖、拖拉等好用功能。

截圖 2021 05 03 下午8.44.49

colab連結

Ben

Python 軟體工程師與量化策略研究員。 鑽研資料工程、網頁後端、資料視覺化、量化交易策略開發。 投資主力在台股市場,量化策略為主、質化分析為輔,追求人機攜做最佳化。逐步將觸角延伸到總經、美股、加密貨幣,朝更全方位的交易人邁進。