CH.04 · 財金程式設計互動教材

Python 金融運算與資料清理

當資料量變大,改用 Python + Pandas 上場——用刻意設計問題的股票 CSV,練習「先驗證、再清理、後計算」的完整流程,算出報酬率、累積報酬率與年化波動度。

clean → calculate → export

本章學習目標

  • 分辨 Python 基本資料型態,用函數封裝金融計算
  • 用 Pandas 完成十步驟資料清理流程
  • 計算每日報酬率、累積報酬率與年化波動度
  • 把分析結果輸出成 CSV/JSON,供網站前台使用

01Python 在金融網站中的角色

前三章完成了複利試算、貨幣時間價值工具及 JSON 債券資料庫,主要在瀏覽器中用 HTML/CSS/JavaScript 完成。但當金融資料逐漸增加——一次讀取數千筆股票價格、計算報酬率與波動度、比較多檔股票——單靠前端 JavaScript 處理會變得不方便。這些工作很適合交由 Python 處理。

比較項目JavaScript(前端)Python(後端/分析)
執行環境瀏覽器(使用者電腦)伺服器或本機執行
資料規模中小型(即時輸入)大量(數千/萬筆)
主要工作按鈕點擊、表單輸入、即時計算、互動圖表資料清理、統計分析、投資組合計算、JSON 輸出
典型工具HTML/CSS/DOM/fetchPandas/NumPy/Matplotlib/Streamlit
本書定位第 1–3 章主角,第 5–8 章視覺化第 4 章主角,後續延伸至第 8、12 章
① 原始資料data/stock-prices.csv
② Python + Pandas清理、計算報酬率與波動度
③ JSON 輸出output/stock-summary.json
④ 前台顯示JavaScript fetch 讀取
i

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
i

型別提示(: float、-> float)不會強制阻止錯誤,但能幫助編輯器(如 VS Code)顯示警告,也讓其他人更容易理解函數用途。raise ValueError 則是主動觸發錯誤,配合 try/except 捕捉。

06股票報酬率的金融觀念

Rₜ = Pₜ / Pₜ₋₁ − 1

Pₜ 為本期價格、Pₜ₋₁ 為前一期價格、Rₜ 為本期報酬率。例如前一期 100 元、本期 105 元:Rₜ = 105/100 − 1 = 5%。

i

本章使用「價格報酬」,未納入股利。若要計算完整投資績效,應使用「還原權息股價」。包含股利的總報酬率公式為 Rₜ = (Pₜ − Pₜ₋₁ + Dₜ) / Pₜ₋₁。

累積報酬率(不能直接加總)——多期累積報酬不能直接把每日報酬率相加。若兩日報酬分別為 +10% 與 −10%:

直接相加 = 0%(錯誤!)

正確公式:(1 + 0.10) × (1 − 0.10) − 1 = −1%
!

連乘後減 1 才是正確的累積報酬。這就是為什麼 Pandas 的 cumprod() 是計算累積報酬的正確方法。

指標日資料年化公式說明
年化平均報酬率日平均報酬率 × 252一年約 252 個交易日(簡化估計)
年化波動度日標準差 × √252報酬率標準差乘以 252 的平方根
i

短期資料(例如僅 10 個交易日)年化後結果可能極不穩定,不應視為可靠的長期預估。年化只是將短期統計機械式放大,不代表未來報酬。

07建立股票價格 CSV(含刻意設計的問題資料)

在 data/ 資料夾建立 stock-prices.csv。此資料刻意包含三種問題,用於練習資料清理:

問題類型哪一行說明
缺漏股價2026-01-16(第一筆)close 欄位空白
重複日期2026-01-16(出現兩次)同日期同代號有兩筆資料
負股價2026-01-19close = −5.00,不合理
程式區塊data/stock-prices.csv(完整版)
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 為 DataFrameDataFrame
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() 函數涵蓋其中步驟 ②~⑧。

步驟步驟名稱主要程式碼初學者注意事項
讀取 CSVpd.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完整股票分析程式

本程式分為六個函數,每個函數負責單一職責。

程式區塊python/stock-analysis.py(完整版含注解)
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         ← 供網站前台使用
i

實際執行結果——原始 CSV 14 筆資料,實際清理後會剩 12 筆:移除 1 筆缺漏股價、1 筆負股價,共移除 2 筆。這裡有個值得注意的細節:程式碼的清理順序是「先刪缺漏值、才計算重複筆數」,而 2026-01-16 那兩筆資料中,缺漏股價的那一筆會先被 dropna() 刪掉,剩下的一筆自然就不重複了——所以本例的「移除重複資料」實際上會是 0 筆,而不是想像中的 1 筆。這正是一個很好的教材案例:**寫程式時務必實際印出中間結果驗證,不能只憑直覺推測資料會剩幾筆**。下面第 13 節的統計數字,都是用本頁「示範分頁」的模擬運算實際跑出來的正確結果。

12完整程式重點解析

函數名稱職責關鍵 Python 語法
validate_columns()確認必要欄位存在,否則 raise ValueErrorset 差集運算
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 筆清理後資料)金融解讀注意
期間總報酬率末價 / 初價 − 1114/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();
i

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,確認用同一環境
FileNotFoundErrorCSV 路徑錯誤或檔名拼錯確認 INPUT_FILE 路徑,或 print(INPUT_FILE) 查看實際路徑
KeyError: 'close'欄位名稱不符(有空格/大小寫)程式已標準化:data.columns = [c.strip().lower() for c in ...]
pandas.errors.ParserErrorCSV 格式錯誤(欄位數不一致)用 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資料品質與金融分析原則

① 資料來源確認來自可靠提供商嗎?
② 欄位檢查有沒有必要欄位?
③ 型態轉換日期、數值是否正確?
④ 缺漏與重複處理依業務邏輯決定,不能補零
⑤ 異常值檢查負股價、極端報酬查證
⑥ 金融計算與判讀樣本期間、資料限制、不保證未來

18關鍵字

Python虛擬環境(venv)變數 int/float/str/boollistdict if/elif/elsefor 迴圈函數(def) 型別提示try/except/raisePandas DataFramepd.read_csv()pd.to_datetime() pd.to_numeric()dropna()drop_duplicates() sort_values()groupby()pct_change() cumprod()std(ddof=1)pathlib.Path json.dump()utf-8-sigensure_ascii 簡單報酬率累積報酬率年化波動度 交易日(252)資料清理缺漏值 異常值

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 種刻意設計的問題資料)

datesymbolclosevolume問題標記

終端機輸出

$ python python/stock-analysis.py

(按上方「執行分析」查看結果)

觀念測驗

共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。

0 / 15 已作答

挑戰任務:程式碼診斷

Python 沒辦法直接在瀏覽器裡執行,所以這裡不是像前三章一樣「執行測試」,而是「程式碼診斷」——看一段藏有問題的程式碼,判斷「這樣寫會發生什麼問題」,選完立即看到詳解與正確寫法。訓練的是讀懂 bug 在哪裡的能力,跟前三章训练修 bug 的能力互相呼應。

任務一:資料分組邏輯

對應教材「十二、六個關鍵技術說明」— groupby 的必要性

難度:理解

下面這段程式碼片段來自 calculate_stock_metrics(),但拿掉了一個重要的部分:


        

如果資料中同時有 STOCK001 和 STOCK002 兩檔股票,這樣寫最可能發生什麼問題?

任務二:資料驗證遺漏

對應教材「六、股票報酬率的金融觀念」與「九、資料清理流程」

難度:診斷

下面這段是 clean_stock_data() 清理流程中,被拿掉了其中一行的版本:


        

拿掉這一行之後,最可能發生什麼問題?

任務三:輸出格式錯誤

對應教材「十二、六個關鍵技術說明」— 中文編碼陷阱

難度:診斷

下面這段程式碼在匯出結果檔案時,少寫了一個關鍵參數:


        

這樣寫最可能發生什麼問題?