2020年2月1日 星期六

scatplot: 一張試算表, 散點圖畫到飽

scatplot web app 範例: 兩政黨各縣市得票比例對照圖 大選過後, 一直想畫 「X黨 vs Y黨在各縣市得票比例對照圖」。 過年時節在家躲疫情, 終於有時間完成了 scatplot 。 這原本是舊文 「三種方式產生 scatter plot / bubble chart」 的範例。 其中的 gnuplot 版跟 python 版都很短。 這次大改版 javascript (web app) 版, 變得很實用, 完全搶了 (未更動的) 其他兩版的鏡頭。 不只是程式宅, 我相信政黨智囊團、 財報型股民、 社會科學家、 自然科學家.. 任何需要以視覺化方式理解數值資料的朋友們也會喜歡這三個 demo 網頁: 太陽系天然衛星軌道常數 兩政黨各縣市得票比例對照圖 股票財報指標圖

一、 操作方式

  1. 可以用滑鼠框起一塊長方形, 畫面會局部放大。
  2. 在畫布上隨處點兩下, 就恢復原來預設的資料全貌圖。
  3. 上述是 plotly 函式庫所提供的眾多功能之一。 畫布上緣右半部的 plotly 工具列, 除了上述預設的縮放 (zoom) 功能之外, 還有其他按鈕。 詳見 plotly 操作介面手冊
  4. 畫布之外的右上角 「設定」 按鈕可以叫出資料設定對話框。
  5. 可以從右側的選單選取不同的政黨 (的得票率) 來當作 X 軸與 Y 軸。
  6. Keep 欄位決定只保留哪些列。 按下 「重畫」 之後, 不只畫布, 連下方的資料表格也會一併更新。
  7. X 與 Y 欄位輸入運算式 事實上右側選單只是偷懶用的簡化版快填選單。 其實 Keep/X/Y/Size 等等每個參數都可以在左側的文字輸入欄內敲進數學運算式。 例如把 X 改成 「中國國民黨+民主進步黨」 把 Y 改成 「台灣民眾黨+時代力量」, 可以看出新竹市與新竹縣對於第三勢力的接受度最高。 (右圖) 或許意謂著基進黨與綠黨等等小黨可以在這裡多耕耘一些?
  8. 下方的表格採用 DataTables 繪製。 欄位名稱按一次, 就以該欄數值由小到大排序; 再按一次改成由大排到小。

二、 基本設定

從網址可以看出來: 用 ".../?c=xyz.json" 可以指定讀取 xyz.json 設定檔。 設定檔甚至可以是你自己管理的遠端網址 (https://...), 但前提是那部伺服器必須已設定 允許跨源資源共享 (CORS)。 不過! 我的程式很簡略, 錯誤檢查做得很粗糙。 如果設定檔改錯了, 畫不出東西, 請先 (例如用 jq 之類的工具) 檢查 .json 檔語法是否正確。 如果還是沒出現正確的圖, 可能就需要打開 瀏覽器的 console 來除錯。

本節以 「兩政黨各縣市得票比例對照圖」 這個範例來解說。 在設定檔 elec20/leg_at_large.json 裡面, 指定從 elec20/leg_at_large.csv 試算表讀出資料。 這張表格是先 把中選會的資料下載回來之後轉檔, 其中的 「不分區立委*.csv」 的縣市資料再以 grep 等文字工具彙整摘要出來、 最後再用 libreoffice 把各政黨得票數改成在各縣市的得票率, 乘以100。 可以看到: 本島縣市當中, 臺南最支持民進黨; 花蓮最支持國民黨。

json 檔裡面有兩大部分: "source" 指定資料來源; "plotly" 指定繪圖參數。 其中 "source" 欄位裡可以指定:

  1. csv: 從哪裡讀取資料試算表。 csv 檔裡面, 可以用 # 開頭放入一列註解。
  2. textcols: 哪些欄位是文字欄位。 其餘一律當成數字欄位。
  3. keep: 預設提供哪些運算式作為資料列篩選保留條件。

而 "plotly" 欄位又分為 "layout"、 "maintrace"、 "statictraces" 三 兩部份。 "layout" 控制整張圖的屬性, 例如全域的字型大小顏色"font"、 圖的標題 "title"、 兩軸 "xaxis" 與 "yaxix" 的各種屬性等等。 關於 "layout" 的完整語法, 請直接參考 plotly 設定手冊。 例如想要查詢 layout.title.font.color 欄位的完整語法, 就在該頁搜尋 "parent: layout.title.font" 反過來說, 想要單獨修改 X 軸刻度的字型與顏色, 可以猜猜看, 在該頁搜尋 "parent: layout.xaxis.tick"。

至於 "maintrace" 則是資料表的圖像顯示, 也是我的程式花最多力氣處理的地方。 其中最有用有趣的是 xaxis.expression、 yaxis.expression、 size.expression。 這三欄可以填寫數學運算式, 決定你要如何從一個或多個資料欄位名稱 (的數學運算式) 算出氣泡的 X 與 Y 座標, 及氣泡大小, 例如我把 maintrace.size.expression 設成 "sqrt(選舉人數)*0.05"。 至於 "maintext" 指定氣泡內的文字, 其中若出現欄位名稱 (例如 「縣市」), 則會把每列資料的該欄文字 (例如 「高雄市」) 或數值代進去。 而 "hovertext" 也一樣, 不過它指定的是滑鼠飄過去時才浮現的文字。 例如另一範例 stock/greg.json 裡面的 "hovertext" 設定成 "代號 名稱", 則滑鼠飄到某檔股票上方時, 會顯示股票代號及名稱 (例如 「1301 台塑」)。 當中如果要換列, 就用 <br>, 例如 "hovertext": "代號<br>名稱"

在各個運算式欄位裡, 可以用哪些運算子和函數? 語法請參考 mathjs 簡介 (含可用運算子清單) 及 函數名稱列表。 唯一的差別是: 除了數字之外, 你還可以用 csv 表格的中英文欄位名稱。 我會幫你轉成 mathjs 看得懂的格式。 但是! 請用夠長、彼此互不為子字串的名字來命名欄位。 錯誤示範: 股利+股票股利 可能會導致錯誤, 因為程式只用單純的字串代換。

[2/6 新增功能] 可以在網址列直接設定某些參數, 例如 source.csv[0]=abc.csv 。 變數名稱對應到 .json 設定檔裡的路徑; 第一層的名稱還可以用開頭一兩個字母簡寫。 網址列的設定優先於 .json 檔裡的設定。 範例: 台灣50配息狀況 (請研究網址)。 如此一來, 可以用同一個設定檔觀察好幾組不同的 .csv 表格資料。

三、 進階設定: 多張 csv 表格、 衍生欄位、 靜態線條

股票 「股價 vs 五年平均息」 散點圖

[2/4 新增本節] 這一節以 「股票財報指標圖」 那個範例來解說更多設定。 這張圖是我個人持的有或想買的某些個股, 橫軸是前一日收盤價; 縱軸是過去五年平均配息; 圈圈越大代表現在 "相對" 比較便宜 (下詳)。

設定檔中的 source.csv 列出三個 csv 檔而非像前兩例只有單一 csv 檔。 第 0 個檔 (greg.csv) 是主表格, 決定了表格的所有列。 它的所有欄位也都會顯示。 其他的 csv 檔則只會貢獻欄位而不會貢獻列: stock/divhist.csv 整理自 histock, 記錄過去五年的配息; 最後一個 https://v.im.cyut.edu.tw/~ckhung/saas/stock/price.csv 則是 「天天自助 calc 便宜股撈明牌」 一文當中介紹過的每日更新上市 (現在已加上上櫃) 股票清單。

表格之間靠著 source.pkey 所指定的欄位連結。 以此例而言, 就是 (股票) 「代號」。 在設定檔裡指定好 source.pkey (主鍵), 我的程式會用它來幫主表格裡的每一列 (十來檔股票) 到其他每個表格查出每個欄位 (例如 stock/divhist.csv 裡面的 y18, ... y14 等等, 以及 .../price.csv 裡面的 price 等欄位) 的對應值、 加入主表格。 (但略過與既有欄位相同名稱的新欄位) 這些欄位預設不會顯示; 但如果想看的話, 也可以從 「column visibility」 按鈕所帶出的選單裡把它們打開。 另外, 程式裡有時需要拿一列當作參考列以便取得各表格的所有欄位, 所以請設定 把 source.samplekey 設成 "1301" (台塑) 或 "2330" (台積電) 之類的, 拿每年有配息數字、 欄位完整的資料列來當參考列。

我最得意的傑作是 source.extracols 。 這個陣列裡的每個元素是一個含等號的字串, 會在表格上創建出一個新的欄位。 等號左邊是欄位名稱, 等號右邊是一個數學運算式。 運算式內可以用到任何一個 csv 檔內的任何一個數值欄位, 也可以用到較早定義 (在 source.extracols 陣列中排在前面) 的其他欄位。 例如:

    "extracols": [
      "五年均息=(y14+y15+y16+y17+y18)/5", 
      "便宜價=五年均息*16",
      "昂貴程度=收盤價/便宜價*100"
    ],

其中 「便宜價」 的計算公式請參考 股海老牛的文章。 然後 plotly.maintrace.xaxis.expr 等三個欄位就可以拿 source.extracols 所定義出來的欄位再來計算 (或直接套用)。 這張圖選用 「收盤價」 當橫軸、 「五年均息」 當縱軸、 「殖利率的倒數」 當圓圈的大小, 只是為了解說方便; 其實呈現的資訊量並不豐富, 因為高配息股大約都落在一直線上, 而且越西北的圈必然越大、 越東南的圈必然越小, 資訊有點重複。 實際上我喜歡拿 「本益比」、 「股價淨值比」、 「殖利率」 等等相關性較低的指標來組合, 各種大小的圈圈會散在各處, 比較可以看出有趣的現象。 關於買股票的其他提醒, 請見 「自助撈明牌」 那篇的第四節。

有時候我們會想在圖上畫幾條不受資料值影響的固定線條。 例如老牛定義便宜價是近五年平均現金股利的 16 倍、 合理價是 20 倍、 昂貴價是 32 倍。 這正好可以在圖上畫出三條直線, 這樣可以很清楚的區分出哪些股票落在便宜區、 哪些落在昂貴區。 (注意: 因為兩軸都採用 log scale, 所以畫出來會是三條平行線而不是交於原點的直線。 直線端點座標一定要用大於零的值。) 此時可用 plotly.statictraces 設一個陣列 ... [2/19 此時可以在 "layout" 裡面分別用 "shapes" 來畫直線、 用 "annotations" 來寫字。 範例請見 greg.json 裡面的 「便宜價」、「合理價」、「昂貴價」 等三條固定的直線。 注意: 如果座標軸採用 log scale, 那麼 annotations 的座標必須先取 log, 例如原本要放在 (80, 5) 的文字, 座標必須寫成 (1.9, 0.7), 亦即 (log(80)/log(10), log(5)/log(10)。 但是 shapes 的座標則不需要取 log。]

如果你覺得這個程式有用, 如果你的資料沒有隱私或機密的問題的話, 請留言分享你的 json 設定檔與 csv 檔網址, 讓更多人看到這個程式可以應用在各種無奇不有的領域! 特別是如果你希望我加入新功能, 請一定要提供測試資料檔及設定檔。

四、 程式設計筆記

以下就真的是給程式宅看的了。

  1. 我對 css 排版超沒興趣的, 所以撿 pure.css 的 Aligned Form 來做最簡單又不失美觀的排版設定。 至於 「設定」 面板的 「展開/收縮」 功能, 是從 這篇超讚教學文 學來的。
  2. 隨便 eval 用戶輸入的字串很危險! 所以採用 mathjs 只允許運算式。 但必須自己先把中文欄位名稱轉換成英文變數名稱。
  3. 像我這樣學 plotly 用 nested object 存設定, 那如果想建立一個 「預設值」, 再跟用戶輸入的設定合併呢? jquery 的 $..extend(true, 預設物件, 用戶輸入物件) 這個函數簡直就是為這個情境而設的完美解!
  4. 需要考慮 「主鍵的值為整數」 的狀況 (例如股票代號): 應存放在 {} 裡面而非 [] 裡面, 避免產生很浪費空間的陣列
  5. 原來 plotly 的圖可以存成 svg!
  6. Plotly 的 3d 圖好酷! 只需要 (1) html 裡面引用 plotly-latest.min.js 而不是較精簡的 plotly-basic-latest.min.js (2) 不只設定 x 與 y, 也加上 z 欄位 (3) 把 trace 的 type 從 scatter 改成 scatter3d。 但是很多東西會失效, 而且轉起來頭很昏, 反而不容易看懂。 放棄。
  7. 若需要用到 FixedHeader 等等 DataTables 的額外功能, 請先到 download 頁面 勾選你要的功能, 再從最下面複製正確的 css 與 js 函式庫網址。
  8. Keep (過濾某些列) 的功能需要呼叫 DataTables 的 $.fn.dataTable.ext.search 函數。 再根據 這個問答.rows({ filter : 'applied'}).data() 取得過濾後的結果。 可是它傳回奇怪的東西: 竟然是 object 而不是 array, 而且還夾雜著其他函數等等, 必須再手動過濾掉。 [2/3 哦, 是我的 js 太弱了。 原來這是一個 array-like object, 可以用 Array.from() 或用 .length 及 for 迴圈轉換成 array。]
  9. DataTables 可以有客製化的外觀
  10. 除錯 CORS 花了好一些時間。 從 firefox 的 console 一直看到 CORS 相關的錯誤訊息, 但明明我的伺服器上的 CORS 幾年前早已設好...? (支持 Enable CORS !) 直到看到 這個回答 才從 console 的 "Network" 頁籤發現: 原來根本是路徑打錯而已。 是 FF 的錯誤訊息不正確。

1 則留言:

  1. 新增幾大功能: 到第二個、第三個 csv 檔查詢更多欄位、 以運算式定義新欄位、 額外的常數線條。 但也因此修改了 .json 設定檔的結構。 請重新下載新版的 .json 檔。 明後天再把新功能補進正文。

    回覆刪除