跳至主要內容
FinLab

Plotly TreeMap 台股版塊地圖製作教學|Python 打造互動式族群漲跌圖

開發動機

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

Finviz 美股板塊地圖示意圖,依板塊與市值顯示紅綠漲跌幅

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

Plotly 範例解析

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

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 截圖,包含 stock_id、報酬率、市值等欄位

有了官方範例,我們很容易依樣畫葫蘆,只要用出同樣邏輯的 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"}

Colab form 語法產生的日期選取 widget 操作介面 產生 date widget

繪圖輸出

台股版塊地圖最終繪圖輸出,依市值與報酬率顯示紅綠族群分佈

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

Colab 程式碼連結

想建立自己的策略?

用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案

免費開始