# 報告要比較三份收盤表？用 skfolio 把公開資料整理成兩張比較表

- 網址：https://blog.valenvan.uk/posts/skfolio-skfolio.html
- 發布日期：2026-10-30
- 測試設備：Mac mini M4 Pro（12 核、64GB）
- 專案：skfolio/skfolio（https://github.com/skfolio/skfolio），BSD-3-Clause 授權
- 測試版本：skfolio 1.12.4 / commit 82114d911cbfc581c2e807903bcc70297bbcff13
- 實測影片：https://www.youtube.com/shorts/OUvdnxcyD0s
- 分類：金融工具區（https://blog.valenvan.uk/finance.html）

> 📌 數據僅限本工具現階段參考用。本文只示範工具怎麼安裝、設定、執行，不是投資建議，也不推薦任何股票。

## 重點摘要

- skfolio 1.12.4 已成功安裝，工具記錄本次原始碼安裝耗時 22.4 秒。
- 證交所2026-09-01至2026-10-29三個示範代號對齊後共38列價格，缺值為0。
- 38列價格轉成37列每日變化後，PearsonDistance 輸出兩張3×3矩陣，示範程式存成CSV。
- 相關矩陣與pandas逐格核對一致；距離對角線出現約1.05e-8浮點殘差，以1e-7容許誤差檢查通過。
- **結論：** 本次實測成功把三欄公開價格轉成兩張可存檔的比較矩陣。
- **適合：** 需要使用Python整理公開收盤資料並保存矩陣檔的人
- **不適合：** 期待不寫程式就能使用完整桌面介面的人

| 實測項目 | 結果 |
|---|---|
| 資料來源 | 證交所公開STOCK_DAY；資料截至2026-10-29 |
| 安裝 | 22.4秒；Python3.12.14 |
| 價格輸入 | 38列×3欄，0缺值 |
| 每日變化輸入 | 37列×3欄 |
| 輸出 | correlation.csv及distance.csv，各3×3 |
| 交叉核對 | 相關表與pandas一致，atol=1e-12 |

收盤後要整理一份資料報告，桌上卻開著三張股票收盤表。日期得對齊，每天的變化得另外整理，換個期間又要重做。這次實測 skfolio 的 PearsonDistance：把同一段期間的公開價格交給它，產出相關表與距離表，再存成可留在報告旁的 CSV。這裡教的是資料整理與工具操作，代號只是示範輸入。

## 下載

來源是 skfolio 官方專案，授權為 BSD-3-Clause。README 要求 Python 3.10 以上；本次實際使用 Python 3.12.14、skfolio 1.12.4，測試設備是 Mac mini M4 Pro（12 核、64GB）。為了重現同一份程式，以下固定到本次受測提交，而不是永遠追最新版。

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'git+https://github.com/skfolio/skfolio.git@82114d911cbfc581c2e807903bcc70297bbcff13'
```

套件會安裝 NumPy、pandas、SciPy、scikit-learn、Plotly 等依賴。本次從原始碼安裝成功，工具記錄為 22.4 秒；下載速度不同，時間也會不同。這個功能不需要券商登入、API 金鑰或下載語言模型。

## 設定

本文示範資料是**證交所公開資料**，使用個股日成交資訊 STOCK_DAY 端點，查詢月份為 2026 年 9 月與 10 月，最後保留到 **2026-10-29**。當時最新完整交易日是 10 月 29 日，沒有把 10 月 30 日盤中資料混進來。

示範輸入為 2330、2317、2412。資料清理包含民國日期轉西元、移除收盤價千分逗號、按日期排列，再把三份表依日期合併。本次得到 38 個共同日期、3 欄價格、0 個缺值。價格是公開原始收盤價，沒有另外作除權息還原處理；日期與來源不同，輸出自然可能不同。

![證交所公開收盤資料對齊後的實測畫面](https://blog.valenvan.uk/img/skfolio-skfolio-article_1.png)

## 跑起來

將下列完整實測程式存為 `test_tool.py`，再執行。第一次會下載並保存六份原始 JSON；再次執行沿用本地快取，避免查核時資料版本變動。`public` 是存檔資料夾，無須啟動網站。

```bash
python test_tool.py
```

```python
import json,time,urllib.request
from pathlib import Path
import pandas as pd
import numpy as np
import skfolio
from skfolio.distance import PearsonDistance
from skfolio.preprocessing import prices_to_returns
codes=['2330','2317','2412']
series=[]
Path('public').mkdir(exist_ok=True)
for code in codes:
 rows=[]
 for month in ['20260901','20261001']:
  url=f'https://www.twse.com.tw/exchangeReport/STOCK_DAY?response=json&date={month}&stockNo={code}'
  cache=Path(f'public/twse_{code}_{month}.json')
  d=json.loads(cache.read_text()) if cache.exists() else json.load(urllib.request.urlopen(url,timeout=40))
  assert d['stat']=='OK',d['stat']
  Path(f'public/twse_{code}_{month}.json').write_text(json.dumps(d,ensure_ascii=False))
  for r in d['data']:
   y,m,day=map(int,r[0].split('/'));date=f'{y+1911:04}-{m:02}-{day:02}'
   if date<='2026-10-29': rows.append((date,float(r[6].replace(',',''))))
  time.sleep(1)
 series.append(pd.Series(dict(rows),name=code))
p=pd.concat(series,axis=1).sort_index()
assert p.index[-1]=='2026-10-29'
missing=int(p.isna().sum().sum()); assert missing==0
p.to_csv('public/prices.csv',index_label='Date')
x=prices_to_returns(p)
t=time.perf_counter();est=PearsonDistance().fit(x);elapsed=time.perf_counter()-t
corr=pd.DataFrame(est.codependence_,index=codes,columns=codes)
distance=pd.DataFrame(est.distance_,index=codes,columns=codes)
corr.to_csv('public/correlation.csv');distance.to_csv('public/distance.csv')
assert np.allclose(corr,x.corr(),atol=1e-12)
assert np.allclose(distance,distance.T) and np.allclose(np.diag(distance),0,atol=1e-7)
result={'version':skfolio.__version__,'source':'TWSE public STOCK_DAY','start':p.index[0],'end':p.index[-1],'codes':codes,'price_rows':len(p),'change_rows':len(x),'missing':missing,'matrix_shape':list(corr.shape),'fit_seconds':round(elapsed,6),'pandas_crosscheck':'PASS','symmetry_and_diagonal':'PASS at atol=1e-7','max_diagonal':float(np.max(np.abs(np.diag(distance)))),'correlation':corr.round(6).values.tolist(),'distance':distance.round(6).values.tolist()}
Path('public/result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
print(json.dumps(result,ensure_ascii=False,indent=2));print('OUTPUT prices.csv, correlation.csv, distance.csv')
```

這次 `prices.csv` 有 38 列價格。`prices_to_returns` 轉換成每天相對前一天的變化，因此少了沒有前一天可比的第一列，剩下 37 列。再把這份資料交給 `PearsonDistance().fit()`，從 `codependence_` 與 `distance_` 取得兩張 3×3 表，輸出成 `correlation.csv`、`distance.csv`。CSV 的寫檔與網頁排版由本次示範程式完成，並非套件自帶的報告網站。

| 實測項目 | 結果 |
|---|---|
| 資料期間 | 2026-09-01 至 2026-10-29 |
| 對齊後價格 | 38 列 × 3 欄，缺值 0 |
| 每日變化 | 37 列 × 3 欄 |
| 兩張輸出表 | 各 3×3，另存 CSV |
| 相關表交叉核對 | 與 pandas 計算逐格一致，容許誤差 1e-12 |

![由真實輸出檔呈現的 skfolio 相關表](https://blog.valenvan.uk/img/skfolio-skfolio-article_2.png)

影片使用的金融工作簿頁面，是我們寫的唯讀檢視器，讀取同一份實測輸出；不是 skfolio 原生操作介面。畫面顯示三位小數，CSV 保留更多位數。這裡不解讀格子中的數字，也不把這個小範例當作完整的研究流程。

## 常見問題

**為什麼 38 天只剩 37 列？** 第一筆價格沒有前一天可比較，轉換後會少一列；這次不是下載漏了一天。若換成別的代號，先檢查共同日期和缺值，不要直接填零。

**距離表的對角線為什麼不是精準的零？** 第一次驗證時，出現約 `1.05367e-08` 的殘差，令預設的近零比較失敗。相關表同一格是 `0.9999999999999998`，後續開根號會放大浮點誤差。確認原始輸出後，我把距離檢查設為 `atol=1e-7`，沒有改原始結果，也沒有在 CSV 裡強制補零。

**資料抓不到怎麼辦？** 程式會檢查證交所回傳的 `stat`；未成功就停止，不能把錯誤頁當價格。先核對網路、月份與代號，再看保存的 JSON。這次端點成功回傳六份資料，不需要登入。

適合願意使用 Python、定期將公開資料整理成矩陣檔的人；如果期待點兩下就有完整桌面軟體，還需要另外設計介面。我會保留這段整理流程，下次換資料期間時再用，同時重新核對日期與缺值。這次沒有測大型資料、其他作業系統或完整投資組合功能。

專案：[skfolio/skfolio](https://github.com/skfolio/skfolio)（BSD-3-Clause）。功能參考：[PearsonDistance 官方文件](https://skfolio.org/generated/skfolio.distance.PearsonDistance.html)；資料來源：[證交所個股日成交資訊](https://www.twse.com.tw/exchangeReport/STOCK_DAY?response=json&date=20261001&stockNo=2330)。
