基金相似度比較

Photo by Alexander Grey on Unsplash

本文重點概要

  • 文章難度:★★★☆☆
  • 使用基金基本資料進行相似度分析
  • 閱讀建議:本文主要將歐式距離、餘弦相似度等公式程式化,公式的推導並不在這次文章的範疇內,建議讀者再閱讀前,先了解歐式距離、餘弦相似度的原理。

前言

投資一定會伴隨著風險,然而並不是所有風險都是無可必免的,每個人心裡都不免會有「損失厭惡」的情節產生,透過分散投資,將資金分散在不同的市場之中,由於資產類別的不同,投資人可以有效管理風險,並減低市場波動對投資組合的影響,本篇文章將主要探討如何使用數據,以客觀科學的角度比較基金之間的相似程度。

編輯環境與模組需求

本文使用 MacOS 作業系統以及 Jupyter Notebook 作為編輯器

# 載入所需套件
import pandas as pd 
import re
import numpy as np 
import tejapi
import plotly.graph_objects as go
import random
import seaborn as sns
import math

# 登入TEJ API
api_key = 'YOUR_KEY'
tejapi.ApiConfig.api_key = api_key
tejapi.ApiConfig.ignoretz = True

資料庫使用

資料導入

fund = tejapi.get('TWN/AATT',
                   paginate = True,
                   opts = {
                       'columns':['coid', 'mdate', 'isin',
                                  'fld006_c', 'fld007_c', 
                                  'fld014_c','fld015',
                                  'fld016_c','un_name_c',
                                  'risk', 'main_flag', 
                                  'fld021', 'currency', 'aunit1',
                                 ]
                   }
                  )
基金資料
資料表預覽

資料處理

本次實作所需要的欄位有:

  • isin : 基金ISIN Code
  • fld014_c : 類型(開放型基金/封閉型基金)
  • fld015 : 型態(目前就基金資金募集地及投資區域)
  • fld016_c : 投資標的
  • risk : 風險收益等級
  • fld021 : 成立資產(千元)
  • currency : 幣別

由於大多數欄位為文字資料,因此在資料預處理時要先將其轉換為序數,一般這樣的做法可以直寫使用ordinal encoding的相關套件,但由於投資標的的類型間存在著風險大小的關係,因此本次實作就自行定義各類別的相應序數,以確保之間的相對風險關係正確。

為了給予序數,我們將資料表內投資標的的所有類別打印出來

fund["fld016_c"].unique()
基金投資標的
基金投資標的類型
style = {
    '':0,
    '保本型': 1,
    '貨幣型': 2,
    '債券型': 3,
    '平衡型': 4,
    'ETF': 5,
    '指數型基金': 6,
    '基金': 7,
    '多重資產': 8,
    '股票型': 9,
    '房地產': 10,
    '產證券化': 11,
    '不動產證券化': 12,
    '科技股': 13,
    '小型股資': 14,
    '能源股票': 15,
    '期貨商品': 16,
}

risk = {
    "":0,
    "RR1":1,
    "RR2":2,
    "RR3":3,
    "RR4":4,
    "RR5":5,
}

area = {
    "國內募集,投資國內":1,
    "國內募集,投資國內外":2,
    "國外募集,投資國內":3,   
}

OorC = {
    "封閉":0,
    "開放":1,
}

# adjust sting data to Ordinal encoding data
fund_adj = fund.copy()
fund_adj["fld015"] = fund_adj["fld015"].apply(lambda x: area.get(x))
fund_adj["fld016_c"] = fund_adj["fld016_c"].apply(lambda x: style.get(x))
fund_adj["risk"] = fund_adj["risk"].apply(lambda x: risk.get(x))
fund_adj["fld014_c"] = fund_adj["fld014_c"].apply(lambda x: OorC.get(x))

再來由於需要使用視覺化圖表呈現基金的相似程度,各數值間需要進行尺度的縮放以符合圖表規格,因此這裡我們對於「成立資產」進行正規化,將數字範圍更改為介於 0 ~ 17 之間( 因為投資標的的最大序數為16 ),經過此處理後「成立資產」僅能呈現不同基金成立金額間的比例差距,不再具有真實的金額意義。

# min-max normalization
size = np.array(fund_adj["fld021"].fillna(0))
size = (size - size.min()) / (size.max() - size.min())*len(style)
fund_adj["fld021"] = size
基金資料
預處理後資料表預覽

雷達圖視覺化

我們篩選幣別為新台幣的基金進行視覺化圖表呈現,本次將以雷達圖呈現基金之間的差異程度,我們隨機在資料集中抽取 10 檔基金來進行相似度比較。

fund = fund[fund["currency"].str.contains("TWD")]

# randomly pick 10 funds
# set the random state
isin_lst = list(fund["isin"].unique())

random.seed(1)
random_isin_lst = random.sample(isin_lst, 10, )

check_lst = random_isin_lst
categories = ['開放型基金/封閉型基金','募集地及投資區域','投資標的',
              '風險', '成立資產']

fig = go.Figure()
data_lst = []
for num, isin in enumerate(check_lst):
    
    data = list(fund_adj[fund_adj["isin"] == isin][["fld014_c", "fld015", "fld016_c", "risk", "fld021"]].iloc[0, :])
    data_lst.append(data)
    
    fig.add_trace(go.Scatterpolar(
          r=data,
          theta=categories,
          fill='toself',
          name=isin
    ))
    
    
fig.update_layout(
  polar=dict(
    radialaxis=dict(
      visible=True,
      range=[0, len(style)]
    )),
  showlegend=True
)

fig.show()
基金相似度
雷達圖圖表呈現

雷達圖可以清晰快速的幫助我們了解各基金項目之間差別,然而它卻無法給予我們一個具體的數值關於到底兩檔基金之間有多相似,因此接下來將使用餘弦相似度 歐式距離 來解決上述問題。

歐式距離

全稱為歐幾里德距離 (Euclidean distance),是一種常見的距離度量方法,用於計算多維空間中任意兩點的直線距離,接下來我們透過程式建立出歐式距離的相關矩陣。

歐式距離
歐式距離 公式
# calculate Euclidean Distance of each couple funds
ED_matrix = np.empty(shape=[len(random_isin_lst), len(random_isin_lst)])
for i in range(len(data_lst)):
    for j in range(len(data_lst)):
        dist = math.dist(data_lst[i], data_lst[j])
        ED_matrix[i, j] = round(dist,5)
print(ED_matrix)
sns.heatmap(ED_matrix, xticklabels = random_isin_lst, yticklabels = random_isin_lst, annot=True, cmap = "flare")

如此,我們已經完成歐式距離的計算,但其以矩陣形式呈現的結果非常不好判讀,因此我們再使用另一種視覺化圖表-熱力圖來顯示結果

基金歐式距離相關矩陣
基金歐式距離相關矩陣

在歐式距離之中,在未將數據標準化或正規化的情況下,數值並沒有上限,0 代表完全相同,而數值越大代表著兩元素間的距離越遠,也就是差異越大。而在熱力圖中,橫軸與縱軸為基金的ISIN code ,而左上至右下的斜對角代表對應到的元素皆為自己,因此距離永遠為 0 。

基金歐式距離熱力圖
基金歐式距離熱力圖

餘弦相似度

餘弦相似度 (Cosine Similarity)通過測量兩個向量的夾角的餘弦值來衡量之間的相似性。 0 度角的餘弦值是 1,代表完全相同,而其他任何角度的餘弦值都不大於 1;並且其最小值是 -1。 從而兩個向量之間的角度的餘弦值確定兩個向量是否大致指向相同的方向。

餘弦相似度公式
餘弦相似度
餘弦相似度值所代表的意義

接著我們將公式程式化,作法與計算歐式距離類似,值得注意的是,餘弦相似度不會因為向量的大小而有差異,這是由於餘弦相似度的計算過程中恰好做了類似正規化的處理

# The measure of cosine similarity will not be affected by the size of the vector
CS_matrix = np.empty(shape=[len(random_isin_lst), len(random_isin_lst)])
for i in range(len(data_lst)):
    for j in range(len(data_lst)):
        A = np.array(data_lst[i])
        B = np.array(data_lst[j])
        cosine = np.dot(A,B)/(norm(A)*norm(B))
        CS_matrix[i, j] = round(cosine,5)
print(CS_matrix)  
sns.heatmap(CS_matrix, xticklabels = random_isin_lst, yticklabels = random_isin_lst, annot=True, cmap = "flare_r")

查看餘弦相似度相關矩陣

餘弦相似度
餘弦相似度相關矩陣
餘弦相似度
餘弦相似度相關矩陣熱力圖

此時眼尖的朋友應該有注意到,歐式距離是數值越小越相似,而餘弦相似度則是數值越大越相似,兩者呈現反關係,因此在熱力圖呈現時應該要講圖表顏色倒轉才比較方便比較兩種計算結果,因此在歐式距離視覺化程式碼中的 cmap = “flare” 在這裡要改為 cmap = “flare_r”。

比較兩張圖,大致上的分布趨勢都相應符合,因為事實上歐式距離就等價於餘弦相似度。

餘弦相似度
餘弦相似度與歐式距離等價證明

先假設空間中有兩點:A、B
針對 A、B 分別做正規化,所得到的結果即為單位向量
以兩單位向量計算餘弦相似度,其分母為 1 所以在此就直接省略
再以兩單位向量計算其歐式距離,經化簡後即可得證

那這樣的話,歐式距離與餘弦相似度的差別到底是什麼呢?
對於歐式距離來說,由於它計算的是兩點之間的直線距離,因此當兩點的趨勢相同,但向量長度不同時,歐式距離就無法反映出這樣的相似性;以本次實作來舉例,當兩檔基金的相似程度極大,只是其中一檔的成立資產較大、另一篇較小時,雖然兩檔基金性質相似,但由於成立資產大小的問題,歐式距離的計算結果仍然會是差距很遠。另一方面,餘弦相似度是計算兩向量的餘弦夾角,因此相似的兩檔基金夾角會很小,因此就可以有效顯示出它們之間的相似性。

歐式距離與餘弦相似度
幾何空間上的歐式距離與餘弦相似度

結語

在相似度的比較結果上我們可以發現幾乎大多數的境內基金之間相似程度都非常高,這非常有可能是由於本次實作僅僅篩選了基金基本資料的一些資訊,因此僅能反映當初基金成立時的狀態,讀者可以在自行加入其他資訊進行計算,例如報酬率、費用率等等,TEJ API 提供完整的基金資訊以及多種取用方式可讓讀者隨心所欲,客製化自己的比較模組。

溫馨提醒,本次介紹與標的僅供參考,不代表任何商品或投資上的建議。之後也會介紹使用TEJ資料庫來建構各式選擇權模型,所以歡迎對選擇權交易有興趣的讀者,選購TEJ E-Shop的相關方案,用高品質的資料庫,建構出適合自己的訂價模型。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing