Plotly-TreeMap|台股版塊地圖|DashBoard製作教學(2)

開發動機

常見美股財經部落客分享Finviz精美強大的圖表,像是美股板塊地圖,一目瞭然各板塊漲跌幅表現,依照顯示顏色紅綠深淺,很快就可以知道市場的熱門族群分佈。族群方塊大小照市值排行,輕易看出各板塊的權值股代表。一張圖可說包山包海,可以快速掌握市場動態,不用一直滑app查半天。

但在台股的網站上,看不太到這類功能,好在學了Python,知道Python在資料處理和資料視覺化非常強大,就來自己開發一下。

Plotly範例解析

搜尋一下Plotly,這類圖表稱為TreeMap,很快就找到精美的範例。

且程式碼精簡的不可思議:

import plotly.express as px
import numpy as np
df = px.data.gapminder().query("year == 2007")
df["world"] = "world" # in order to have a single root node
fig = px.treemap(df, path=['world', 'continent', 'country'], values='pop',
                  color='lifeExp', hover_data=['iso_alpha'],
                  color_continuous_scale='RdBu',
                  color_continuous_midpoint=np.average(df['lifeExp'], weights=df['pop']))
fig.show()

稍微解析一下px.treemap,完整用法可參考:

帶入DataFrame,path為Top Down排序,最前面的value是最外圍的方塊,範例由世界-洲-國家來排序,由外而內樹狀分佈。

value是決定方塊大小,這裡是用pop(人口)。

color是用lifeExp(壽命)來決定,方塊顏色會依照該國平均壽命大小對應到color_continuous_scale的樣式。

hover_data是互動圖表的功能,滑鼠移到方塊上時閃現的資訊,通常不用特別設定。

color_continuous_midpoin是決定lifeExp對應color_continuous_scale顏色條的中間點數值為何?例如範例中的’RdBu’色條,數值由大到小是藍-白-紅,而我們希望用全世界人類平均餘命來定義中間點白色數值,則可寫成np.average(df[‘lifeExp’], weights=df[‘pop’]),越大於平均的數字越藍,越小的越紅。

台股版塊程式撰寫

主程式

class TwStockTreeMap:

    def __init__(self, close, basic_info, start=None, end=None):
        self.start = start
        self.end = end
        self.close = close
        self.basic_info = basic_info

    # dataframe filter by selected date
    def df_date_filter(self, df, start=None, end=None):
        if start:
            df = df[df.index >= start]
        if end:
            df = df[df.index <= end]
        return df

    # map stock_name from basic info(stock_id +name)
    def map_stock_name(self, basic_info, s):
        target = basic_info[basic_info['stock_id'].str.find(s) > -1]
        if len(target) > 0:
            s = target['stock_id'].values[0]
        return s

    def create_data(self):
        close_data = self.df_date_filter(self.close, self.start, end=self.end)
        return_ratio = (close_data.iloc[-1] / close_data.iloc[0]).dropna().replace(np.inf, 0)
        return_ratio = round((return_ratio - 1) * 100, 2)
        return_ratio = pd.concat([return_ratio, close_data.iloc[-1]], axis=1).dropna()
        return_ratio = return_ratio.reset_index()
        return_ratio.columns = ['stock_id', 'return_ratio', 'close']
        return_ratio['stock_id'] = return_ratio['stock_id'].apply(lambda s: self.map_stock_name(basic_info, s))
        return_ratio = return_ratio.merge(self.basic_info[['stock_id', '產業類別', '市場別', '實收資本額(元)']], how='left',
                                          on='stock_id')
        return_ratio = return_ratio.rename(columns={'產業類別': 'category', '市場別': 'market', '實收資本額(元)': 'base'})
        return_ratio['market_value'] = round(return_ratio['base'] / 10 * return_ratio['close'] / 100000000, 2)
        return_ratio = return_ratio.dropna(thresh=5)
        return_ratio['country'] = 'TW-Stock'
        return_ratio['return_ratio_text_info']=return_ratio['return_ratio'].astype(str).apply(lambda s: '+' + s if '-' not in s else s) + '%'
        return return_ratio

    def create_fig(self, relative_market_strength=False):
        df = self.create_data()
        if relative_market_strength is True:
            color_continuous_midpoint = np.average(df['return_ratio'], weights=df['base'])
        else:
            color_continuous_midpoint = 0
        fig = px.treemap(df, path=['country', 'market', 'category', 'stock_id'], values='market_value',
                         color='return_ratio',
                         color_continuous_scale='Tealrose',
                         color_continuous_midpoint=color_continuous_midpoint,
                         custom_data=['return_ratio_text_info','close'],
                         title=f'TW-Stock Market TreeMap({self.start}~{self.end})',
                         width=1350, 
                         height=900)
        
        fig.update_traces(textposition='middle center', 
                          textfont_size=24,
                          texttemplate= "%{label}<br>%{customdata[0]}<br>%{customdata[1]}"
                          )
        return fig
   

資料處理邏輯

有了官方範例,我們很容易依樣畫葫蘆,只要用出同樣邏輯的dataframe就可套入。path的[‘world’, ‘continent’, ‘country’]換成[‘country’, ‘market’, ‘category’, ‘stock_id’],ex:台股-上市-半導體-台積電,分為四層。

邏輯確定後就可開始撰寫dataframe,只會用到收盤價算報酬率、企業基本資料(公開資訊觀測站資料源)抓股名、上市櫃分類、產業類別、實收資本額。計算市值時將單位化成億元單位。ETF由於沒有產業類別與實收資本額,會被排除掉。詳見create_data()程式碼。

繪圖程式修改

詳見create_fig()的內容,短短的程式碼就能弄出厲害的圖表:

1.value採用market_value(市值)。

2.color採用return_ratio(報酬率)。

3.color_continuous_scale採用’Tealrose’,因為由大到小,是紅到綠,與習慣相符。

4.color_continuous_midpoin設為0,因為我們希望漲的都是紅色系,跌的都是綠色系,若用原本範例的畫法,則是以相對市場平均強弱來顯現。

5.設置custom_data=[‘return_ratio_text_info’,’close’],客製化texttemplate會用到,不然只會有stock_id

6.加入title、width、height畫布樣式,設定標題與長寬。

7.fig.update_traces(textposition='middle center', textfont_size=24,texttemplate= "%{label}<br>%{customdata[0]}<br>%{customdata[1]}),textposition設定資料顯示字體位置,textfont_size字體大小,texttemplate使用custom_data自訂方塊內顯示的文字資訊。

plotly_express_treemap強大的地方是還會幫你自動計算整個板塊市值的市場占比,且點擊每一個方塊會自動縮放,放大各類股和企業區塊,有一些市值比較小的公司一開始看不到,透過點擊放大,就可以看出資料,這是原先finviz無法達成的互動性效果。

程式若在Colab跑,可用colab的form語法跑出簡易的操作介面,選取區間日期,查整體市場該期間的報酬情況。

#@title 台股漲跌與市值板塊圖
start= '2021-05-20' #@param {type:"date"}
end = '2021-05-21' #@param {type:"date"}
relative_market_strength = "False" #@param ["False", "True"] {type:"raw"}
產生date widget

繪圖輸出

是不是跟Finviz有87分像呢?有興趣的同學也可以把報酬率改成本益比或其他指標,或改成多指標選單,讓板塊圖的功能更加完善喔!

Colab程式碼連結

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

上一篇
下一篇

Genie

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