01Python 在金融網站中的角色
前三章完成了複利試算、貨幣時間價值工具及 JSON 債券資料庫,主要在瀏覽器中用 HTML/CSS/JavaScript 完成。但當金融資料逐漸增加——一次讀取數千筆股票價格、計算報酬率與波動度、比較多檔股票——單靠前端 JavaScript 處理會變得不方便。這些工作很適合交由 Python 處理。
| 比較項目 | JavaScript(前端) | Python(後端/分析) |
|---|---|---|
| 執行環境 | 瀏覽器(使用者電腦) | 伺服器或本機執行 |
| 資料規模 | 中小型(即時輸入) | 大量(數千/萬筆) |
| 主要工作 | 按鈕點擊、表單輸入、即時計算、互動圖表 | 資料清理、統計分析、投資組合計算、JSON 輸出 |
| 典型工具 | HTML/CSS/DOM/fetch | Pandas/NumPy/Matplotlib/Streamlit |
| 本書定位 | 第 1–3 章主角,第 5–8 章視覺化 | 第 4 章主角,後續延伸至第 8、12 章 |
Python 負責計算,JSON 負責資料交換,JavaScript 負責前台顯示。三者各司其職,不互相取代。
02建立 Python 執行環境
確認 Python 版本並建立虛擬環境——虛擬環境讓每個專案使用自己的套件版本,避免不同專案互相影響。
# 確認 Python 是否已安裝(在 PowerShell 輸入) py --version py -3.12 --version # 若安裝多個版本,可指定版本 # 步驟 1:建立虛擬環境(在專案根目錄執行) py -3.12 -m venv .venv # 步驟 2:啟動虛擬環境 .venv\Scripts\Activate.ps1 # 啟動成功後,提示字元前會出現 (.venv) # 若 PowerShell 禁止執行腳本,先輸入: Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass # 步驟 3:安裝 Pandas python -m pip install pandas # 步驟 4:確認安裝成功 python -c "import pandas as pd; print(pd.__version__)" # 步驟 5:離開虛擬環境 deactivate
更新後的專案資料夾結構
financial-platform/ ├─ index.html ├─ time-value.html ├─ bond-database.html ├─ css/ ├─ js/ ├─ data/ │ ├─ bonds.json │ └─ stock-prices.csv ← 第四章新增(原始股票資料) ├─ python/ │ ├─ basic-python.py ← Python 基礎練習 │ └─ stock-analysis.py ← 股票分析主程式 ├─ output/ ← Python 分析輸出目錄 └─ README.md
03Python 基本語法
變數與命名規則
# ✓ 好的命名:英文、有意義、底線分隔 principal = 100000 # 本金 annual_rate = 0.05 # 年報酬率(小數) years = 10 # 投資年數 stock_symbol = '2330' # 股票代號(字串) # ✗ 不建議:單字母、數字開頭、無意義名稱 a = 100000 # 不知道是什麼 1st_price = 0 # 數字開頭,Python 語法錯誤 data123 = [] # 語意不清
Python 常見資料型態
| 型態 | 範例 | 金融用途 | 注意事項 |
|---|---|---|---|
| int(整數) | 100000 / 252 | 面額、股數、交易日數 | 沒有小數點 |
| float(浮點數) | 0.05 / 100.5 | 報酬率、股價、票面利率 | 有小數點 |
| str(字串) | "2330" | 股票代號、日期文字 | 用引號包住 |
| bool(布林值) | True / False | 是否上架、是否有效 | 第一個字母大寫(Python 專屬) |
| list(清單) | [100, 102, 101, 105] | 多日股價序列 | 用中括號,索引從 0 開始 |
| dict(字典) | {"symbol":"2330","price":100.5} | 單筆金融資料 | 鍵值對,用大括號 |
# 複利終值計算
principal = 100000
annual_rate = 0.05
years = 10
future_value = principal * (1 + annual_rate) ** years # ** 是次方
print(f"投資終值為:{future_value:,.0f} 元") # 千分位格式
# 輸出:投資終值為:162,889 元
# 清單與迴圈
prices = [100, 102, 101, 105]
print(prices[0]) # 第一筆:100(索引從 0 開始)
print(prices[-1]) # 最後一筆:105
# 字典
stock = {"symbol": "STOCK001", "price": 100.5, "active": True}
print(stock["price"]) # 100.5
04條件判斷與迴圈
# if / elif / else:金融資料合理性檢查
annual_return = -0.12
if annual_return > 0:
print("正報酬")
elif annual_return == 0:
print("報酬率為零")
else:
print("負報酬")
# 輸出:負報酬
# 金融程式常見驗證
price = -5
if price <= 0:
print("股價必須大於 0,請排除此筆資料")
Python 使用縮排(4 個空格或 1 個 Tab)表示程式區塊,不使用 { }。縮排錯誤是 Python 初學者最常遇到的 IndentationError。
# for 迴圈計算多期報酬
prices = [100, 102, 101, 105]
# range(1, len(prices)) 從索引 1 開始,因為第一筆沒有前期資料
for index in range(1, len(prices)):
previous_price = prices[index - 1]
current_price = prices[index]
daily_return = current_price / previous_price - 1
print(f"第 {index} 期報酬率:{daily_return:.2%}")
# 輸出:
# 第 1 期報酬率:2.00%
# 第 2 期報酬率:-0.49%
# 第 3 期報酬率:3.96%
05使用函數封裝金融計算
| 好處 | 說明 | 金融範例 |
|---|---|---|
| 降低重複程式 | 相同邏輯只寫一次 | 報酬率公式不需每處重寫 |
| 提高可讀性 | 函數名稱說明用途 | calculate_return() 比一行公式直覺 |
| 容易測試 | 每個函數可獨立驗證 | 可單獨測試 calculate_return(100, 105) |
| 容易修改 | 改函數即可影響所有呼叫處 | 加入股利計算只需改函數本體 |
def calculate_return(
previous_price: float, # 型別提示:預期為浮點數
current_price: float
) -> float: # 回傳值型別提示
"""計算簡單價格報酬率。"""
if previous_price <= 0:
raise ValueError("前一期價格必須大於 0")
if current_price <= 0:
raise ValueError("本期價格必須大於 0")
return current_price / previous_price - 1
# 正常使用
result = calculate_return(100, 105)
print(f"{result:.2%}") # 輸出:5.00%
# 例外處理:前一期股價為 0
try:
result = calculate_return(0, 105)
except ValueError as error:
print(f"計算失敗:{error}")
# 輸出:計算失敗:前一期價格必須大於 0
型別提示(: float、-> float)不會強制阻止錯誤,但能幫助編輯器(如 VS Code)顯示警告,也讓其他人更容易理解函數用途。raise ValueError 則是主動觸發錯誤,配合 try/except 捕捉。
06股票報酬率的金融觀念
Pₜ 為本期價格、Pₜ₋₁ 為前一期價格、Rₜ 為本期報酬率。例如前一期 100 元、本期 105 元:Rₜ = 105/100 − 1 = 5%。
本章使用「價格報酬」,未納入股利。若要計算完整投資績效,應使用「還原權息股價」。包含股利的總報酬率公式為 Rₜ = (Pₜ − Pₜ₋₁ + Dₜ) / Pₜ₋₁。
累積報酬率(不能直接加總)——多期累積報酬不能直接把每日報酬率相加。若兩日報酬分別為 +10% 與 −10%:
直接相加 = 0%(錯誤!) 正確公式:(1 + 0.10) × (1 − 0.10) − 1 = −1%
連乘後減 1 才是正確的累積報酬。這就是為什麼 Pandas 的 cumprod() 是計算累積報酬的正確方法。
| 指標 | 日資料年化公式 | 說明 |
|---|---|---|
| 年化平均報酬率 | 日平均報酬率 × 252 | 一年約 252 個交易日(簡化估計) |
| 年化波動度 | 日標準差 × √252 | 報酬率標準差乘以 252 的平方根 |
短期資料(例如僅 10 個交易日)年化後結果可能極不穩定,不應視為可靠的長期預估。年化只是將短期統計機械式放大,不代表未來報酬。
07建立股票價格 CSV(含刻意設計的問題資料)
在 data/ 資料夾建立 stock-prices.csv。此資料刻意包含三種問題,用於練習資料清理:
| 問題類型 | 哪一行 | 說明 |
|---|---|---|
| 缺漏股價 | 2026-01-16(第一筆) | close 欄位空白 |
| 重複日期 | 2026-01-16(出現兩次) | 同日期同代號有兩筆資料 |
| 負股價 | 2026-01-19 | close = −5.00,不合理 |
date,symbol,close,volume 2026-01-02,STOCK001,100.00,1200000 2026-01-05,STOCK001,102.00,1350000 2026-01-06,STOCK001,101.50,980000 2026-01-07,STOCK001,104.00,1500000 2026-01-08,STOCK001,103.00,1100000 2026-01-09,STOCK001,106.00,1700000 2026-01-12,STOCK001,108.00,1650000 2026-01-13,STOCK001,107.00,1250000 2026-01-14,STOCK001,110.00,1900000 2026-01-15,STOCK001,112.00,2100000 2026-01-16,STOCK001,,1300000 ← ① close 欄位空白(缺漏值) 2026-01-16,STOCK001,113.00,1300000 ← ② 同一日期重複出現 2026-01-19,STOCK001,-5.00,900000 ← ③ 負股價(無效資料) 2026-01-20,STOCK001,114.00,1450000
08Pandas 與 DataFrame 基礎
DataFrame 可以理解為「具有欄名與列索引的資料表」,類似 Excel 工作表。讀取 CSV 後,每欄對應一個欄位(date、symbol、close、volume),每列代表一筆交易記錄。
import pandas as pd
# 讀取 CSV
data = pd.read_csv("data/stock-prices.csv")
# 查看基本資訊
print(data.head()) # 前 5 筆
print(data.tail()) # 後 5 筆
print(data.info()) # 欄位、型態、非空值數量
print(data.describe()) # 數值欄位統計摘要
print(data.shape) # (14, 4) = 14 列 4 欄
print(data.columns) # 欄位名稱清單
print(data.isna().sum()) # 每欄缺漏值數量
Pandas 常用方法對照表
| 方法 | 用途 | 回傳結果 |
|---|---|---|
| pd.read_csv(path) | 讀取 CSV 為 DataFrame | DataFrame |
| data.dropna(subset=[...]) | 刪除指定欄位有缺漏的列 | DataFrame |
| data.drop_duplicates(subset=) | 刪除重複列 | DataFrame |
| data.sort_values(by=[...]) | 依欄位排序 | DataFrame |
| data.groupby(col) | 依欄位分組 | GroupBy 物件 |
| group.pct_change() | 計算相鄰期百分比變化(報酬率) | Series |
| series.cumprod() | 連乘積(累積報酬) | Series |
| series.std(ddof=1) | 樣本標準差(波動度) | float |
| data.to_csv(path) | 匯出為 CSV 檔案 | None(寫入檔案) |
09金融資料清理流程(十步驟)
金融資料進入分析前,必須依序完成以下步驟。本章的 clean_stock_data() 函數涵蓋其中步驟 ②~⑧。
| 步驟 | 步驟名稱 | 主要程式碼 | 初學者注意事項 |
|---|---|---|---|
| ① | 讀取 CSV | pd.read_csv() | — |
| ② | 欄位驗證 | 確認欄位存在 | 缺少必要欄位 → raise ValueError |
| ③ | 轉換日期 | pd.to_datetime(errors='coerce') | 無法辨識 → 轉為 NaT |
| ④ | 轉換數值 | pd.to_numeric(errors='coerce') | 非數字文字 → 轉為 NaN |
| ⑤ | 刪除缺漏值 | dropna(subset=[...]) | 本章做法:直接刪除缺漏行 |
| ⑥ | 刪除重複資料 | drop_duplicates(...) | 同日同股重複 → 保留最後一筆 |
| ⑦ | 排除無效價格 | data[data['close']>0] | 股價 ≤ 0 → 排除 |
| ⑧ | 排序 | sort_values(by=[...]) | 報酬率計算必須先排序! |
| ⑨ | 計算報酬率 | pct_change()、cumprod()、std(ddof=1) | — |
| ⑩ | 匯出結果 | to_csv()、json.dump() | — |
清理順序很重要:必須先刪除缺漏值,再刪除重複資料,最後排序,才能確保報酬率計算正確。不同順序可能產生不同的清理結果。
# ① 讀取
data = pd.read_csv("data/stock-prices.csv")
# ② 欄位名稱標準化(去空白 + 轉小寫)
data.columns = [col.strip().lower() for col in data.columns]
# ③ 轉換日期
data["date"] = pd.to_datetime(data["date"], errors="coerce")
# errors='coerce':無法辨識的日期 → NaT(Not a Time)
# ④ 轉換收盤價為數值
data["close"] = pd.to_numeric(data["close"], errors="coerce")
# errors='coerce':非數字文字 → NaN
# ⑤ 刪除日期或收盤價缺漏的資料列
data = data.dropna(subset=["date", "close"])
# ⑥ 刪除重複資料(同日期同股票,保留最後一筆)
data = data.drop_duplicates(subset=["date", "symbol"], keep="last")
# ⚠️ 正式系統應先查明重複原因,再決定保留哪一筆
# ⑦ 排除負股價
data = data[data["close"] > 0]
# ⑧ 依股票代號與日期排序(報酬率計算必要步驟)
data = data.sort_values(by=["symbol", "date"]).reset_index(drop=True)
缺漏值處理方式比較
| 處理方式 | 程式寫法 | 適合場景 | 金融資料注意 |
|---|---|---|---|
| 刪除缺漏列 | dropna() | 缺漏比例低,且缺漏隨機 | 本章做法,適合教學示範 |
| 前值填補 | ffill() | 股價因休市或假日造成缺漏 | 可能掩蓋真實問題 |
| 插值 | interpolate() | 連續型資料短暫缺漏 | 股價插值可能失真 |
| 補零 | fillna(0) | ⚠️ 幾乎不建議用於金融價格 | 補零會產生假報酬(−100%) |
金融價格資料缺漏不能任意補零。補 0 後,股價會顯示從正常值跌到 0(−100%),再從 0 漲到正常值(報酬率無窮大),產生完全虛假的報酬序列。
10完整股票分析程式
本程式分為六個函數,每個函數負責單一職責。
from pathlib import Path
import json
import math
import sys
import pandas as pd
# ── 路徑設定(使用 Path 確保跨平台相容)──────────
BASE_DIR = Path(__file__).resolve().parent.parent
INPUT_FILE = BASE_DIR / 'data' / 'stock-prices.csv'
OUTPUT_DIR = BASE_DIR / 'output'
CLEANED_FILE = OUTPUT_DIR / 'cleaned-stock-prices.csv'
SUMMARY_CSV_FILE = OUTPUT_DIR / 'stock-summary.csv'
SUMMARY_JSON_FILE = OUTPUT_DIR / 'stock-summary.json'
TRADING_DAYS_PER_YEAR = 252 # 一年約 252 個交易日
# ── ① 欄位驗證 ──────────────────────────────────
def validate_columns(data: pd.DataFrame, required: set[str]) -> None:
missing = required - set(data.columns)
if missing:
raise ValueError(f"資料缺少必要欄位:{', '.join(sorted(missing))}")
# ── ② 資料清理(回傳清理後 DataFrame + 清理報告)
def clean_stock_data(data: pd.DataFrame) -> tuple[pd.DataFrame, dict]:
original_rows = len(data)
data = data.copy() # 建立副本,不改動原始資料
data.columns = [col.strip().lower() for col in data.columns]
validate_columns(data, {"date", "symbol", "close", "volume"})
data["symbol"] = data["symbol"].astype("string").str.strip().str.upper()
data["date"] = pd.to_datetime(data["date"], errors="coerce")
data["close"] = pd.to_numeric(data["close"], errors="coerce")
data["volume"] = pd.to_numeric(data["volume"], errors="coerce")
missing_before = {col: int(val) for col, val in data.isna().sum().items()}
data = data.dropna(subset=["date", "symbol", "close"])
dup_count = int(data.duplicated(subset=["date", "symbol"], keep="last").sum())
data = data.drop_duplicates(subset=["date", "symbol"], keep="last")
invalid_price = int((data["close"] <= 0).sum())
data = data[data["close"] > 0]
invalid_vol = int((data["volume"] < 0).sum())
data.loc[data["volume"] < 0, "volume"] = pd.NA
data = data.sort_values(by=["symbol", "date"]).reset_index(drop=True)
report = {
"original_rows": original_rows, "cleaned_rows": len(data),
"removed_rows": original_rows - len(data), "duplicate_removed": dup_count,
"invalid_prices": invalid_price, "invalid_volumes": invalid_vol,
"missing_before": missing_before,
}
return data, report
# ── ③ 計算報酬率與統計指標 ──────────────────────
def calculate_stock_metrics(data: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame]:
result = data.copy()
result["daily_return"] = (
result.groupby("symbol")["close"].pct_change(fill_method=None)
)
result["growth_factor"] = 1 + result["daily_return"]
result["cumulative_return"] = (
result.groupby("symbol")["growth_factor"].cumprod() - 1
)
summary_rows = []
for symbol, group in result.groupby("symbol"):
valid_returns = group["daily_return"].dropna()
first_price = float(group["close"].iloc[0])
last_price = float(group["close"].iloc[-1])
summary_rows.append({
"symbol": symbol,
"start_date": group["date"].min().strftime("%Y-%m-%d"),
"end_date": group["date"].max().strftime("%Y-%m-%d"),
"observations": int(len(group)),
"first_price": first_price, "last_price": last_price,
"total_return": last_price / first_price - 1,
"average_daily_return": float(valid_returns.mean()),
"annualized_average_return": float(valid_returns.mean()) * TRADING_DAYS_PER_YEAR,
"daily_volatility": float(valid_returns.std(ddof=1)),
"annualized_volatility": float(valid_returns.std(ddof=1)) * math.sqrt(TRADING_DAYS_PER_YEAR),
"maximum_daily_return": float(valid_returns.max()),
"minimum_daily_return": float(valid_returns.min()),
"positive_days": int((valid_returns > 0).sum()),
"negative_days": int((valid_returns < 0).sum()),
"zero_return_days": int((valid_returns == 0).sum()),
})
return result, pd.DataFrame(summary_rows)
# ── ⑥ 主程式(try/except 確保錯誤有友善提示)───
def main() -> None:
try:
if not INPUT_FILE.exists():
raise FileNotFoundError(f"找不到輸入檔案:{INPUT_FILE}")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
raw_data = pd.read_csv(INPUT_FILE)
cleaned_data, report = clean_stock_data(raw_data)
if cleaned_data.empty:
raise ValueError("清理後沒有可分析的資料。")
analyzed_data, summary_data = calculate_stock_metrics(cleaned_data)
analyzed_data.to_csv(CLEANED_FILE, index=False, encoding="utf-8-sig")
summary_data.to_csv(SUMMARY_CSV_FILE, index=False, encoding="utf-8-sig")
json_records = convert_summary_to_json(summary_data)
with open(SUMMARY_JSON_FILE, "w", encoding="utf-8") as f:
json.dump(json_records, f, ensure_ascii=False, indent=2)
print_cleaning_report(report)
print(f"\n分析完成,輸出至:{OUTPUT_DIR}")
except FileNotFoundError as e: print(f'檔案錯誤:{e}'); sys.exit(1)
except pd.errors.EmptyDataError: print('CSV 沒有內容'); sys.exit(1)
except pd.errors.ParserError as e: print(f'CSV 解析錯誤:{e}'); sys.exit(1)
except ValueError as e: print(f'資料驗證錯誤:{e}'); sys.exit(1)
except Exception as e: print(f'未預期錯誤:{e}'); sys.exit(1)
if __name__ == "__main__":
main()
完整版(含 convert_summary_to_json() 與 print_cleaning_report() 兩個輔助函數)請參考課本第十節,此處節錄核心流程以利閱讀。
11執行 Python 分析程式
# 步驟 1:進入專案資料夾 cd C:\financial-platform # 步驟 2:啟動虛擬環境 .venv\Scripts\Activate.ps1 # 步驟 3:執行分析程式 python python\stock-analysis.py # 或指定版本 py -3.12 python\stock-analysis.py # 執行成功後,output/ 資料夾應出現: # output/ # ├─ cleaned-stock-prices.csv ← 清理後逐日資料(含報酬率) # ├─ stock-summary.csv ← 股票統計摘要 # └─ stock-summary.json ← 供網站前台使用
實際執行結果——原始 CSV 14 筆資料,實際清理後會剩 12 筆:移除 1 筆缺漏股價、1 筆負股價,共移除 2 筆。這裡有個值得注意的細節:程式碼的清理順序是「先刪缺漏值、才計算重複筆數」,而 2026-01-16 那兩筆資料中,缺漏股價的那一筆會先被 dropna() 刪掉,剩下的一筆自然就不重複了——所以本例的「移除重複資料」實際上會是 0 筆,而不是想像中的 1 筆。這正是一個很好的教材案例:**寫程式時務必實際印出中間結果驗證,不能只憑直覺推測資料會剩幾筆**。下面第 13 節的統計數字,都是用本頁「示範分頁」的模擬運算實際跑出來的正確結果。
12完整程式重點解析
| 函數名稱 | 職責 | 關鍵 Python 語法 |
|---|---|---|
| validate_columns() | 確認必要欄位存在,否則 raise ValueError | set 差集運算 |
| clean_stock_data() | 十步驟清理流程,回傳清理後 DataFrame + 報告 | copy()、to_datetime()、drop_duplicates() |
| calculate_stock_metrics() | 計算每日報酬率、累積報酬率、波動度等統計指標 | groupby()、pct_change()、cumprod()、std(ddof=1) |
| convert_summary_to_json() | 將 DataFrame 轉換為 JSON 可序列化格式 | to_dict(orient='records')、pd.isna() |
| main() | 主程式:整合所有函數,含完整錯誤處理 | try/except、Path、json.dump() |
六個關鍵技術說明
# 1. pathlib.Path:跨平台路徑(Windows/macOS/Linux 都相容)
BASE_DIR = Path(__file__).resolve().parent.parent
# 2. data.copy():建立副本,避免函數改動原始 DataFrame
data = data.copy() # 重要!不加 copy() 可能有 SettingWithCopyWarning
# 3. groupby + pct_change:依股票分組計算報酬率
result["daily_return"] = (
result.groupby("symbol")["close"].pct_change(fill_method=None)
)
# 若不加 groupby,不同股票的最後一期與下一股票的第一期會被比較!
# 4. cumprod():累積乘積(計算累積報酬率)
# (1 + 5%) × (1 - 2%) × (1 + 3%) = 1.05 × 0.98 × 1.03 = 1.059...
# → 累積報酬率 ≈ 5.9%
# 5. std(ddof=1):樣本標準差(分母為 n-1)
# ddof=0 = 母體標準差(分母 n)
# ddof=1 = 樣本標準差(分母 n-1,金融分析常用)
# 6. json.dump 的兩個重要參數
json.dump(records, f, ensure_ascii=False, indent=2)
# ensure_ascii=False:保留中文字元(否則中文會變成 \uXXXX)
# indent=2:縮排格式,讓 JSON 容易閱讀
13分析結果的金融判讀
以下數字取自本章 12 筆清理後資料的實際計算結果(與本頁「示範分頁」模擬運算一致):
| 指標 | 計算方式 | 實際數值(12 筆清理後資料) | 金融解讀注意 |
|---|---|---|---|
| 期間總報酬率 | 末價 / 初價 − 1 | 114/100 − 1 = 14.00% | 價格報酬,未含股利 |
| 平均日報酬率 | 各日報酬算術平均 | 約 1.21% | 短期樣本,僅供參考 |
| 年化平均報酬率 | 日平均 × 252 | 約 304%(⚠️ 不穩定) | 僅 11 個報酬觀察值,年化後失真 |
| 年化波動度 | 日標準差 × √252 | 約 22.9% | 反映報酬率波動幅度 |
| 最大單日報酬 | 報酬率序列最大值 | 約 2.91% | 若異常大,需查證原因 |
| 最小單日報酬 | 報酬率序列最小值 | 約 −0.96% | 下跌幅度最大的一天 |
本章只有 12 個交易日資料(11 個報酬率觀察值),年化平均報酬率約 304% 是機械式放大,完全不能代表長期預期報酬。報告應同時標示樣本期間與觀察值數量。
14將 Python 結果提供網站前台使用
Python 輸出的 stock-summary.json 可直接被網站前台的 JavaScript fetch 讀取,完成完整的資料流程閉環。
async function loadStockSummary() {
try {
const response = await fetch("output/stock-summary.json");
if (!response.ok) throw new Error("載入失敗");
const data = await response.json();
// data[0] 即第一檔股票的分析結果
const stock = data[0];
console.log(`股票代號:${stock.symbol}`);
console.log(`期間總報酬:${(stock.total_return * 100).toFixed(2)}%`);
console.log(`年化波動度:${(stock.annualized_volatility * 100).toFixed(2)}%`);
} catch (error) {
console.error("股票摘要載入失敗:", error);
}
}
loadStockSummary();
Python 輸出的 JSON 中,報酬率欄位(total_return 等)以小數保存(0.14 = 14%),JavaScript 顯示時需乘以 100 再格式化。這與第三章的 coupon_rate 一致。
15實務案例
(一)兩檔股票總報酬相同,但風險不同
甲股票一年總報酬 10%,價格波動較小;乙股票一年總報酬也是 10%,但期間曾下跌 30% 再回升。若只看期初與期末價格,績效相同;考慮投資人的風險承受度與資金需求,兩者差異很大。股票分析應同時觀察:總報酬、波動度(標準差)、最大跌幅(最大回撤)、報酬分布(正/負日數)。
(二)資料缺漏不能補零
| 日期 | 股價 | 若補零後的報酬率 | 問題 |
|---|---|---|---|
| 1月1日 | 100 | — | 起始 |
| 1月2日 | 缺漏→補 0 | −100% | 100→0,報酬率 −100%(假) |
| 1月3日 | 120 | 報酬率無窮大 | 0→120,除以 0,無法計算 |
金融價格資料缺漏補零,會產生 −100% 和「無窮大」的假報酬,完全失去分析價值。正確做法是刪除缺漏列,或使用前值填補(需確認商業邏輯)。
(三)重複日期的處理原則
同一股票同一日期出現兩筆收盤價,可能是盤中價與收盤價混用、資料更新版本或系統錯誤。本章做法:保留最後一筆。正式系統應先查明原因,確認哪一筆是正式資料。
(四)極端報酬的查證原因
若某日報酬率異常大(例如 +30%),可能原因包括:真實市場重大事件、除權息(股價調整)、股票分割或合併、幣別錯誤或資料來源切換、還原權息方式不同、資料輸入錯誤。極端值不一定是錯誤,但一定需要查證。自動化程式應標記極端值供人工確認,而不是直接刪除或保留。
16常見錯誤與除錯
| 錯誤訊息 / 症狀 | 最可能原因 | 修正方式 |
|---|---|---|
| ModuleNotFoundError: No module named 'pandas' | Pandas 未安裝,或執行環境不同 | python -m pip install pandas,確認用同一環境 |
| FileNotFoundError | CSV 路徑錯誤或檔名拼錯 | 確認 INPUT_FILE 路徑,或 print(INPUT_FILE) 查看實際路徑 |
| KeyError: 'close' | 欄位名稱不符(有空格/大小寫) | 程式已標準化:data.columns = [c.strip().lower() for c in ...] |
| pandas.errors.ParserError | CSV 格式錯誤(欄位數不一致) | 用 Excel 或文字編輯器確認 CSV 格式正確 |
| 日期轉換後出現 NaT | 日期格式無法辨識 | 用 data[data['date'].isna()] 找出問題列 |
| 報酬率出現 inf(無窮大) | 前一期股價為 0,除以 0 | 程式已排除 close <= 0,確認清理流程正確執行 |
| Excel 開啟 CSV 亂碼 | 未使用 utf-8-sig 編碼 | to_csv(..., encoding="utf-8-sig") |
| IndentationError | 縮排不一致(Tab 與空格混用) | 統一使用 4 個空格,不要混用 Tab |
| SettingWithCopyWarning | 對 DataFrame 切片直接設值 | 加上 data = data.copy() 後再修改 |
17資料品質與金融分析原則
- 不要直接覆蓋原始 CSV,建議分開保存 data/raw/ 與 data/cleaned/
- 清楚記錄刪除了哪些資料、為何刪除、保留哪一筆重複資料
- 比較多檔股票時,確保資料期間一致,否則會產生不公平比較
- 只使用收盤價計算的是「價格報酬」,完整投資績效應考慮股利與除權息
- 年化報告應同時標示樣本期間、觀察值數量與資料限制,不能只呈現年化數字
18關鍵字
19實作練習
- 在 CSV 中增加五筆正確股票價格資料,執行程式並確認清理後筆數增加
- 記錄期初價格、期末價格,手動計算總報酬率,與程式輸出比較
- 開啟 output/stock-summary.csv,確認欄位內容與標準差數值
- 開啟 output/stock-summary.json,確認格式與 CSV 相同資料但格式不同
- 查看最大單日報酬與最小單日報酬對應的日期
- 增加最高收盤價、最低收盤價與收盤價中位數到摘要報告
- 增加上漲日比率(positive_days / observations)
- 增加報酬率超過 5% 的異常日期清單(輸出為 CSV)
- 將負成交量資料另存為錯誤報告 CSV
- 將清理報告輸出為 JSON(cleaning-report.json)
- 在 CSV 加入第二檔股票(STOCK002),確認 groupby 能正確分別計算報酬率
- 計算兩檔股票的相關係數(pandas corr())
- 增加 20 日移動平均(rolling(20).mean())並匯出
- 使用 Matplotlib 繪製股票收盤價折線圖(plt.plot)
- 使用 Streamlit 建立互動式分析介面(st.line_chart)
- 建立自動資料驗證報告,列出所有不符合規則的資料
因為 Python 無法在瀏覽器中直接執行,本章的挑戰任務(04 分頁)改成「程式碼診斷」形式——針對「六個關鍵技術說明」與「常見錯誤與除錯」表格中的典型陷阱設計選擇題,答題後立即看到詳解與正確寫法,訓練的是「讀懂 bug 在哪裡」的能力;實際動手執行程式,請在自己電腦安裝好 Python + Pandas 之後練習。
本章結語——本章建立了 Python 金融資料分析的基本能力。從變數、資料型態、條件判斷、迴圈、函數,到 Pandas 的 DataFrame 操作,完成了一個可實際執行的股票分析程式。
金融分析的核心不只是套用公式。若日期重複、價格缺漏、欄位錯置或數值不合理,即使程式沒有報錯,分析結果仍可能完全失真。本章強調「先驗證、再清理、後計算」的流程,並透過刻意設計的問題 CSV(缺漏、重複、負股價)練習每一個清理步驟。
完成的 stock-analysis.py 能自動讀取 CSV、清理資料、計算統計指標,並輸出 CSV 與 JSON,供網站前台的 JavaScript 讀取——這就是 Python(計算)+ JSON(交換)+ JavaScript(顯示)的完整資料流程。
下一章進入金融資料視覺化,使用 Chart.js 與 Plotly 將複利成長、債券價格與殖利率、股票累積報酬及風險報酬關係,轉化為更容易理解的互動圖形。
—股票分析模擬執行
重要提醒:Python 程式無法直接在瀏覽器裡執行,所以這個分頁不是真的在跑 Python。下面用 JavaScript 忠實重現了課本 stock-analysis.py 的清理與計算邏輯(已經用真正的 pandas 3.0.2 逐行核對過所有數字),讓你可以在網頁上直接看到「如果照著課本的程式碼執行,會得到什麼結果」。真正上課請在自己電腦安裝 Python + Pandas 後執行 python/stock-analysis.py。
原始資料:data/stock-prices.csv(14 筆,含 3 種刻意設計的問題資料)
| date | symbol | close | volume | 問題標記 |
|---|
終端機輸出
$ python python/stock-analysis.py (按上方「執行分析」查看結果)
清理後資料:output/cleaned-stock-prices.csv
| date | symbol | close | volume |
|---|
統計摘要:output/stock-summary.json
—觀念測驗
共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。
挑戰任務:程式碼診斷
Python 沒辦法直接在瀏覽器裡執行,所以這裡不是像前三章一樣「執行測試」,而是「程式碼診斷」——看一段藏有問題的程式碼,判斷「這樣寫會發生什麼問題」,選完立即看到詳解與正確寫法。訓練的是讀懂 bug 在哪裡的能力,跟前三章训练修 bug 的能力互相呼應。
任務一:資料分組邏輯
對應教材「十二、六個關鍵技術說明」— groupby 的必要性
下面這段程式碼片段來自 calculate_stock_metrics(),但拿掉了一個重要的部分:
如果資料中同時有 STOCK001 和 STOCK002 兩檔股票,這樣寫最可能發生什麼問題?
任務二:資料驗證遺漏
對應教材「六、股票報酬率的金融觀念」與「九、資料清理流程」
下面這段是 clean_stock_data() 清理流程中,被拿掉了其中一行的版本:
拿掉這一行之後,最可能發生什麼問題?
任務三:輸出格式錯誤
對應教材「十二、六個關鍵技術說明」— 中文編碼陷阱
下面這段程式碼在匯出結果檔案時,少寫了一個關鍵參數:
這樣寫最可能發生什麼問題?