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

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連結供各位參考。

程式重點

整理對帳單格式

每家券商對帳單格式不同,於process_data()修改欄位,自行複寫function,demo使用新光證券富貴角7號下載的對帳單,stock_id、date、pnl(已實現損益)是必要欄位,date換成西元年格式,之後dash的DatePickerRange會用到日期查詢互動的部分。


# 欄位修改:'日期':'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

繪圖物件

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

plotly.express是用在比較不需要客製化單一圖表的情況,封裝精簡,但在本次範例是以plotly.graph_objects來繪圖做比較複雜的設定。以下將幾個會用到的圖表官方教學連結附上,多是參考官方範例做調整

make_subplots :製作子圖畫布,注意每個圖表使用的type不一樣,例如’domain’ 是用在 Pie,預設是xy,適用座標圖。colspan和rawspan可以做子圖佔比控制,和html語法一樣。subplot_titles可控制子圖標題,這邊加上賺賠與損益總金額道子圖的標題上。若子圖有雙Y軸,記得設定”secondary_y”: True。

pie-charts and sunburst:sunburst可以做整合性的圓餅圖資料呈現。製作虧損標的的分佈圖時,注意要將值轉成絕對值,圖表顯示不出負數出來。

bar :棒狀圖,繪製每日已實現損益變化和標的損益排序

    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'), 1, 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'), 1, 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',
        ), 1, 3)

        # 每日已實現損益變化
        fig.add_trace(go.Bar(x=date_group.index, y=date_group['pnl'], name="date", marker_color="#636EFA"), 3, 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"), 4, 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

Run Dash

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。

    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')

之後執行程式,loading跑完之後,美美的互動圖表就生成啦!右側的label列可以點掉標的,圖表就會扣除該標的重新計算比例,右上有plotly自帶的工具列,有縮放、截圖、拖拉等好用功能。

完整程式碼的colab連結,之後還會製作其他dashboard小工具給大家喔~

喜歡我們的文章的話,那更別錯過我們精心製作的優質系列課程喔!

上一篇
下一篇

Genie

Python elf, love magic of python. Explore the financial world.