Python 在過去十年間已發展成為資料科學領域的主流程式語言。這並非偶然,而是因為 Python 具備幾項關鍵優勢:簡潔直觀的語法降低了學習門檻,豐富的科學計算函式庫生態系統提供了強大的工具支援,活躍的社群持續貢獻高品質的開源專案。對於希望進入資料科學領域的學習者而言,掌握 Python 的基礎知識是不可或缺的第一步。

本文將從 Python 的核心語法和資料結構開始,逐步深入到資料讀寫、蒙地卡羅模擬等應用場景,最後探討線性代數在資料科學中的基礎地位。線性代數不僅是機器學習演算法的數學基礎,也是理解資料轉換、降維分析等進階技術的關鍵。透過結合 NumPy 和 Pandas 等函式庫的實作範例,讀者將能夠建立從理論到實踐的完整知識體系。

Python 程式設計基礎概念

Python 的設計哲學強調程式碼的可讀性和簡潔性。這種設計理念體現在語言的各個層面,從縮排作為程式區塊的界定方式,到直觀的語法結構。理解這些基礎概念是有效運用 Python 進行資料科學工作的前提。

函式定義與型別標註

函式是程式碼組織的基本單位,它將特定功能封裝起來以便重複使用。Python 3.5 之後引入的型別標註功能,雖然不會在執行時強制檢查型別,但能夠提高程式碼的可讀性和可維護性,並支援靜態型別檢查工具。

# 定義帶有型別標註的函式
# 型別標註說明參數和回傳值的預期型別
# 這有助於程式碼文件化和 IDE 自動完成

def calculate_compound_interest(
    principal: float,
    rate: float,
    years: int
) -> float:
    """
    計算複利終值

    Args:
        principal: 本金金額
        rate: 年利率(小數形式,如 0.05 表示 5%)
        years: 投資年數

    Returns:
        投資終值
    """
    # 使用複利公式:A = P(1 + r)^n
    # 其中 A 是終值,P 是本金,r 是利率,n 是期數
    return principal * (1 + rate) ** years

def format_currency(amount: float, currency: str = "TWD") -> str:
    """
    將金額格式化為貨幣字串

    Args:
        amount: 金額數值
        currency: 貨幣代碼,預設為新台幣

    Returns:
        格式化後的貨幣字串
    """
    # 使用格式化字串將數值轉換為帶千分位的金額
    # :,.2f 表示千分位分隔符和兩位小數
    return f"{currency} {amount:,.2f}"

# 實際使用範例
if __name__ == "__main__":
    # 設定投資參數
    initial_investment = 100000.0  # 本金 10 萬
    annual_rate = 0.06             # 年利率 6%
    investment_period = 10         # 投資 10 年

    # 計算終值
    final_value = calculate_compound_interest(
        principal=initial_investment,
        rate=annual_rate,
        years=investment_period
    )

    # 格式化輸出結果
    print(f"初始投資: {format_currency(initial_investment)}")
    print(f"年利率: {annual_rate * 100}%")
    print(f"投資期間: {investment_period} 年")
    print(f"投資終值: {format_currency(final_value)}")

    # 計算獲利
    profit = final_value - initial_investment
    roi = (profit / initial_investment) * 100
    print(f"總獲利: {format_currency(profit)}")
    print(f"投資報酬率: {roi:.2f}%")

這段程式碼展示了 Python 函式定義的幾個重要特性。型別標註使用冒號後接型別名稱的語法,箭頭後面則標註回傳型別。文件字串(docstring)採用三重引號,遵循 Google 風格的格式,清楚說明參數和回傳值。預設參數值的設定讓函式呼叫更加彈性,使用者可以選擇性地覆寫預設值。

核心資料結構的特性與應用

Python 提供了幾種內建的資料結構,各有不同的特性和適用場景。理解這些資料結構的差異,對於選擇正確的工具解決問題至關重要。

列表(List)是最常用的資料結構,它是一個有序的可變序列。列表可以包含任意型別的元素,支援索引存取、切片操作和各種修改方法。由於列表的可變性,它適合用於需要頻繁增刪元素的場景。

元組(Tuple)與列表類似,但它是不可變的。一旦建立,就無法修改其內容。這種不可變性使元組適合用於表示固定的資料集合,例如座標點、RGB 色彩值等。元組也可以作為字典的鍵,而列表則不行。

字典(Dictionary)是一種鍵值對的集合,提供快速的查找能力。字典使用雜湊表實作,因此平均情況下查找、插入和刪除操作的時間複雜度都是 O(1)。字典適合用於需要透過標識符快速存取資料的場景。

# 列表的進階操作示範
# 列表是有序、可變的序列

def analyze_stock_prices(prices: list) -> dict:
    """
    分析股票價格資料

    Args:
        prices: 股票價格列表

    Returns:
        包含各種統計指標的字典
    """
    if not prices:
        return {"error": "價格資料為空"}

    # 基本統計計算
    n = len(prices)
    total = sum(prices)
    average = total / n

    # 找出最高和最低價
    highest = max(prices)
    lowest = min(prices)

    # 計算價格變動
    # 使用列表推導式計算每日漲跌
    daily_changes = [
        prices[i] - prices[i-1]
        for i in range(1, n)
    ]

    # 統計上漲和下跌天數
    up_days = sum(1 for change in daily_changes if change > 0)
    down_days = sum(1 for change in daily_changes if change < 0)

    return {
        "count": n,
        "average": round(average, 2),
        "highest": highest,
        "lowest": lowest,
        "range": highest - lowest,
        "up_days": up_days,
        "down_days": down_days,
        "volatility": round(max(daily_changes) - min(daily_changes), 2) if daily_changes else 0
    }

# 元組的應用:表示不可變的資料結構
# 例如地理座標、日期範圍等

def calculate_distance(point1: tuple, point2: tuple) -> float:
    """
    計算兩點之間的歐幾里得距離

    Args:
        point1: 第一點座標 (x, y)
        point2: 第二點座標 (x, y)

    Returns:
        兩點間的距離
    """
    # 解包元組取得各座標分量
    x1, y1 = point1
    x2, y2 = point2

    # 應用距離公式:sqrt((x2-x1)^2 + (y2-y1)^2)
    distance = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
    return distance

# 字典的進階應用:巢狀結構和方法操作
def create_portfolio(stocks: dict, cash: float) -> dict:
    """
    建立投資組合結構

    Args:
        stocks: 股票持倉字典 {股票代碼: (股數, 成本價)}
        cash: 現金部位

    Returns:
        完整的投資組合字典
    """
    portfolio = {
        "holdings": {},
        "cash": cash,
        "total_cost": 0.0
    }

    # 處理每檔股票的持倉資訊
    for symbol, (shares, cost_price) in stocks.items():
        position_cost = shares * cost_price
        portfolio["holdings"][symbol] = {
            "shares": shares,
            "cost_price": cost_price,
            "position_cost": position_cost
        }
        portfolio["total_cost"] += position_cost

    portfolio["total_cost"] += cash

    return portfolio

# 實際使用示範
if __name__ == "__main__":
    # 股價分析範例
    sample_prices = [150.5, 152.3, 151.8, 155.2, 154.0, 156.8, 158.5]
    analysis = analyze_stock_prices(sample_prices)

    print("股價分析結果:")
    for key, value in analysis.items():
        print(f"  {key}: {value}")

    # 距離計算範例
    taipei = (25.0330, 121.5654)     # 台北座標
    kaohsiung = (22.6273, 120.3014)  # 高雄座標
    dist = calculate_distance(taipei, kaohsiung)
    print(f"\n台北到高雄的座標距離: {dist:.4f} 度")

    # 投資組合範例
    my_stocks = {
        "2330": (1000, 580.0),   # 台積電
        "2317": (2000, 105.0),   # 鴻海
        "2454": (500, 850.0)     # 聯發科
    }
    my_portfolio = create_portfolio(my_stocks, 50000.0)

    print("\n投資組合:")
    print(f"  現金: {my_portfolio['cash']:,.0f}")
    print(f"  總成本: {my_portfolio['total_cost']:,.0f}")
    for symbol, info in my_portfolio["holdings"].items():
        print(f"  {symbol}: {info['shares']} 股 @ {info['cost_price']}")

這些範例展示了資料結構在實際應用中的使用方式。列表推導式提供了簡潔的方式來建立新列表,生成器運算式則能在不佔用大量記憶體的情況下處理資料。元組的解包功能讓程式碼更加清晰易讀。字典的巢狀結構可以表達複雜的資料關係。

資料讀寫與 Pandas 基礎操作

在資料科學工作流程中,資料的讀取和儲存是不可或缺的環節。Python 的 Pandas 函式庫提供了強大的資料操作能力,其核心資料結構 DataFrame 可以輕鬆處理表格式資料。

Pandas 支援多種資料格式的讀寫,包括 CSV、Excel、JSON、SQL 資料庫等。其設計理念借鑑了 R 語言的 data.frame,但在效能和功能上都有顯著提升。理解 Pandas 的基本操作對於資料前處理和探索性分析至關重要。

import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# 建立示範資料
# 模擬一個月的股票交易資料

def generate_trading_data(days: int = 30) -> pd.DataFrame:
    """
    生成模擬的股票交易資料

    Args:
        days: 交易天數

    Returns:
        包含交易資料的 DataFrame
    """
    # 設定隨機種子以確保結果可重現
    np.random.seed(42)

    # 生成日期範圍
    # 使用 pandas 的 date_range 函式建立營業日序列
    start_date = datetime(2024, 1, 1)
    dates = pd.date_range(
        start=start_date,
        periods=days,
        freq='B'  # B 表示營業日
    )

    # 生成價格資料
    # 使用隨機遊走模型模擬價格變動
    initial_price = 100.0
    returns = np.random.normal(0.001, 0.02, days)  # 平均報酬 0.1%,標準差 2%
    prices = initial_price * np.cumprod(1 + returns)

    # 生成成交量資料
    # 假設成交量服從對數常態分布
    volumes = np.random.lognormal(mean=15, sigma=0.5, size=days).astype(int)

    # 計算每日高低價
    # 假設日內波動約為收盤價的 2%
    daily_volatility = prices * 0.02
    highs = prices + np.random.uniform(0, 1, days) * daily_volatility
    lows = prices - np.random.uniform(0, 1, days) * daily_volatility

    # 建立 DataFrame
    df = pd.DataFrame({
        'date': dates,
        'open': prices * (1 + np.random.uniform(-0.01, 0.01, days)),
        'high': highs,
        'low': lows,
        'close': prices,
        'volume': volumes
    })

    # 設定日期為索引
    df.set_index('date', inplace=True)

    return df

def analyze_trading_data(df: pd.DataFrame) -> dict:
    """
    分析交易資料並計算各種指標

    Args:
        df: 包含 OHLCV 資料的 DataFrame

    Returns:
        分析結果字典
    """
    # 計算日報酬率
    # 使用 pct_change 方法計算百分比變動
    df['return'] = df['close'].pct_change()

    # 計算移動平均線
    # 5 日和 20 日移動平均是常用的技術指標
    df['ma5'] = df['close'].rolling(window=5).mean()
    df['ma20'] = df['close'].rolling(window=20).mean()

    # 計算波動率
    # 使用滾動標準差作為波動率的代理指標
    df['volatility'] = df['return'].rolling(window=5).std() * np.sqrt(252)

    # 彙總統計
    summary = {
        'period': f"{df.index[0].strftime('%Y-%m-%d')} to {df.index[-1].strftime('%Y-%m-%d')}",
        'trading_days': len(df),
        'start_price': round(df['close'].iloc[0], 2),
        'end_price': round(df['close'].iloc[-1], 2),
        'total_return': round((df['close'].iloc[-1] / df['close'].iloc[0] - 1) * 100, 2),
        'avg_daily_return': round(df['return'].mean() * 100, 4),
        'volatility': round(df['return'].std() * np.sqrt(252) * 100, 2),
        'max_drawdown': round(calculate_max_drawdown(df['close']) * 100, 2),
        'total_volume': int(df['volume'].sum()),
        'avg_volume': int(df['volume'].mean())
    }

    return df, summary

def calculate_max_drawdown(prices: pd.Series) -> float:
    """
    計算最大回撤

    Args:
        prices: 價格序列

    Returns:
        最大回撤比例
    """
    # 計算累積最高價
    cummax = prices.cummax()

    # 計算從高點的回撤
    drawdown = (prices - cummax) / cummax

    # 返回最大回撤(負值)
    return drawdown.min()

# 資料儲存與讀取示範
def save_and_load_demo(df: pd.DataFrame, filename: str = "trading_data"):
    """
    示範資料的儲存和讀取操作

    Args:
        df: 要儲存的 DataFrame
        filename: 檔案名稱(不含副檔名)
    """
    # 儲存為 CSV 格式
    # CSV 是最通用的格式,可被大多數工具讀取
    csv_path = f"{filename}.csv"
    df.to_csv(csv_path)
    print(f"資料已儲存至 {csv_path}")

    # 從 CSV 讀取
    # parse_dates 參數自動解析日期欄位
    # index_col 指定哪一欄作為索引
    df_from_csv = pd.read_csv(
        csv_path,
        parse_dates=['date'],
        index_col='date'
    )

    # 儲存為 Parquet 格式
    # Parquet 是高效的列式儲存格式,適合大型資料集
    parquet_path = f"{filename}.parquet"
    df.to_parquet(parquet_path)
    print(f"資料已儲存至 {parquet_path}")

    # 從 Parquet 讀取
    df_from_parquet = pd.read_parquet(parquet_path)

    return df_from_csv, df_from_parquet

# 主程式
if __name__ == "__main__":
    # 生成交易資料
    trading_df = generate_trading_data(30)

    # 顯示資料概覽
    print("交易資料概覽:")
    print(trading_df.head(10))
    print(f"\n資料維度: {trading_df.shape}")
    print(f"\n資料型別:\n{trading_df.dtypes}")

    # 進行分析
    analyzed_df, summary = analyze_trading_data(trading_df)

    print("\n===== 交易分析摘要 =====")
    for key, value in summary.items():
        label = key.replace('_', ' ').title()
        if 'return' in key or 'volatility' in key or 'drawdown' in key:
            print(f"{label}: {value}%")
        else:
            print(f"{label}: {value}")

    # 顯示最近幾天的完整資料
    print("\n最近 5 個交易日的詳細資料:")
    print(analyzed_df[['close', 'return', 'ma5', 'ma20', 'volatility']].tail())

這段程式碼展示了 Pandas 在金融資料分析中的典型應用。rolling 方法用於計算滾動統計量,這在技術分析中非常常見。pct_change 方法計算百分比變動,是計算報酬率的標準方式。資料的儲存支援多種格式,CSV 適合通用場景,而 Parquet 格式則提供更好的壓縮率和讀取效能。

蒙地卡羅模擬在金融分析的應用

蒙地卡羅模擬是一種利用隨機抽樣來估計數學問題解的方法。這種方法特別適合處理具有不確定性的問題,例如金融市場的價格預測、風險評估和選擇權定價。蒙地卡羅方法的核心思想是:透過大量的隨機模擬,可以近似出系統的統計特性。

在股票價格模擬中,最常用的模型是幾何布朗運動(Geometric Brownian Motion)。這個模型假設股票價格的對數報酬率服從常態分布,並且價格變動具有連續性。雖然這個假設在現實中不完全成立(例如市場會有跳空),但它提供了一個合理的起點來理解價格動態。

import numpy as np
import matplotlib.pyplot as plt
from typing import Tuple

def simulate_gbm_paths(
    S0: float,
    mu: float,
    sigma: float,
    T: float,
    dt: float,
    n_paths: int
) -> np.ndarray:
    """
    使用幾何布朗運動模型模擬股票價格路徑

    幾何布朗運動的隨機微分方程為:
    dS = μS dt + σS dW

    其中:
    - S 是股票價格
    - μ 是漂移率(期望報酬率)
    - σ 是波動率
    - W 是維納過程(布朗運動)

    離散化後的模擬公式為:
    S(t+dt) = S(t) * exp((μ - σ²/2)*dt + σ*√dt*Z)
    其中 Z ~ N(0,1)

    Args:
        S0: 初始股票價格
        mu: 年化漂移率(期望報酬率)
        sigma: 年化波動率
        T: 模擬期間(年)
        dt: 時間步長(年)
        n_paths: 模擬路徑數量

    Returns:
        模擬的價格路徑陣列,形狀為 (n_steps+1, n_paths)
    """
    # 計算時間步數
    n_steps = int(T / dt)

    # 初始化價格陣列
    # 第一維是時間,第二維是路徑
    prices = np.zeros((n_steps + 1, n_paths))
    prices[0] = S0

    # 生成隨機增量
    # 使用標準常態分布生成 Z 值
    # 每個時間步和每條路徑都需要一個獨立的隨機數
    Z = np.random.standard_normal((n_steps, n_paths))

    # 計算漂移項和擴散項
    # 漂移項考慮了 Ito 引理的修正項 -σ²/2
    drift = (mu - 0.5 * sigma ** 2) * dt
    diffusion = sigma * np.sqrt(dt)

    # 逐步模擬價格路徑
    for t in range(1, n_steps + 1):
        # 應用離散化的 GBM 公式
        prices[t] = prices[t-1] * np.exp(drift + diffusion * Z[t-1])

    return prices

def analyze_simulation_results(
    paths: np.ndarray,
    confidence_level: float = 0.95
) -> dict:
    """
    分析模擬結果並計算風險指標

    Args:
        paths: 價格路徑陣列
        confidence_level: 信賴水準,用於計算 VaR

    Returns:
        包含各種風險指標的字典
    """
    # 取得最終價格分布
    final_prices = paths[-1]
    initial_price = paths[0, 0]

    # 計算報酬率分布
    returns = (final_prices - initial_price) / initial_price

    # 計算基本統計量
    mean_return = np.mean(returns)
    std_return = np.std(returns)
    median_return = np.median(returns)

    # 計算 VaR(Value at Risk)
    # VaR 是在給定信賴水準下的最大預期損失
    var_level = 1 - confidence_level
    var = np.percentile(returns, var_level * 100)

    # 計算 CVaR(Conditional Value at Risk)
    # CVaR 是超過 VaR 的條件期望損失
    cvar = np.mean(returns[returns <= var])

    # 計算獲利機率
    prob_profit = np.mean(returns > 0)

    # 計算百分位數
    percentiles = {
        5: np.percentile(final_prices, 5),
        25: np.percentile(final_prices, 25),
        50: np.percentile(final_prices, 50),
        75: np.percentile(final_prices, 75),
        95: np.percentile(final_prices, 95)
    }

    return {
        'mean_return': mean_return,
        'std_return': std_return,
        'median_return': median_return,
        f'var_{int(confidence_level*100)}': var,
        f'cvar_{int(confidence_level*100)}': cvar,
        'prob_profit': prob_profit,
        'mean_final_price': np.mean(final_prices),
        'percentiles': percentiles
    }

def run_monte_carlo_simulation():
    """
    執行完整的蒙地卡羅模擬並展示結果
    """
    # 設定模擬參數
    S0 = 100.0        # 初始價格
    mu = 0.08         # 年化期望報酬率 8%
    sigma = 0.25      # 年化波動率 25%
    T = 1.0           # 模擬期間 1 年
    dt = 1/252        # 時間步長為 1 個交易日
    n_paths = 10000   # 模擬 10000 條路徑

    print("蒙地卡羅模擬參數:")
    print(f"  初始價格: {S0}")
    print(f"  期望年報酬率: {mu*100}%")
    print(f"  年化波動率: {sigma*100}%")
    print(f"  模擬期間: {T} 年")
    print(f"  模擬路徑數: {n_paths}")

    # 執行模擬
    np.random.seed(123)  # 設定隨機種子以確保可重現性
    paths = simulate_gbm_paths(S0, mu, sigma, T, dt, n_paths)

    # 分析結果
    results = analyze_simulation_results(paths)

    print("\n模擬結果分析:")
    print(f"  平均報酬率: {results['mean_return']*100:.2f}%")
    print(f"  報酬率標準差: {results['std_return']*100:.2f}%")
    print(f"  中位數報酬率: {results['median_return']*100:.2f}%")
    print(f"  獲利機率: {results['prob_profit']*100:.2f}%")
    print(f"  95% VaR: {results['var_95']*100:.2f}%")
    print(f"  95% CVaR: {results['cvar_95']*100:.2f}%")

    print("\n最終價格分布:")
    for pct, value in results['percentiles'].items():
        print(f"  第 {pct} 百分位: {value:.2f}")

    return paths, results

# 執行模擬
if __name__ == "__main__":
    paths, results = run_monte_carlo_simulation()

這個蒙地卡羅模擬展示了幾個重要的金融概念。幾何布朗運動模型是選擇權定價(如 Black-Scholes 模型)的基礎。Ito 引理的修正項 -σ²/2 確保了對數價格的期望值正確。VaR 和 CVaR 是現代風險管理中的核心指標,監管機構通常要求金融機構計算這些數值。

以下是蒙地卡羅模擬流程的示意圖:

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:設定模擬參數\n(S0, μ, σ, T, dt);

:初始化價格陣列\nprices[0] = S0;

:生成隨機數矩陣\nZ ~ N(0, 1);

while (t < T?) is (是)
    :計算價格增量\ndS = μS dt + σS√dt Z;
    :更新價格\nS(t+dt) = S(t) + dS;
    :t = t + dt;
endwhile (否)

:收集最終價格分布;

:計算統計指標\n(VaR, CVaR, 報酬率);

:輸出風險分析報告;

stop

@enduml

線性代數基礎概念

線性代數是現代資料科學和機器學習的數學基礎。從簡單的線性迴歸到複雜的深度神經網路,幾乎所有的演算法都建立在線性代數的概念之上。理解向量、矩陣和線性變換,不僅有助於正確使用現有的工具,也能幫助我們設計更有效率的解決方案。

向量空間與向量運算

向量可以從幾何和代數兩個角度來理解。在幾何上,向量表示空間中的方向和大小;在代數上,向量是一組有序數的集合。向量空間定義了向量可以進行的運算,包括加法和純量乘法,並且這些運算必須滿足特定的公理。

import numpy as np

def demonstrate_vector_operations():
    """
    展示向量的基本運算
    """
    # 建立向量
    # NumPy 陣列是 Python 中表示向量的標準方式
    v1 = np.array([3, 4])
    v2 = np.array([1, 2])

    print("向量運算示範")
    print("=" * 40)
    print(f"向量 v1: {v1}")
    print(f"向量 v2: {v2}")

    # 向量加法
    # 對應元素相加
    v_sum = v1 + v2
    print(f"\nv1 + v2 = {v_sum}")

    # 向量減法
    v_diff = v1 - v2
    print(f"v1 - v2 = {v_diff}")

    # 純量乘法
    # 每個元素乘以純量
    scalar = 2.5
    v_scaled = scalar * v1
    print(f"{scalar} * v1 = {v_scaled}")

    # 向量長度(範數)
    # L2 範數:sqrt(x1² + x2² + ...)
    norm_v1 = np.linalg.norm(v1)
    print(f"\n||v1|| = {norm_v1}")

    # 單位向量
    # 將向量除以其長度得到單位向量
    unit_v1 = v1 / norm_v1
    print(f"v1 的單位向量: {unit_v1}")
    print(f"單位向量的長度: {np.linalg.norm(unit_v1)}")

    # 內積(點積)
    # 結果是純量:v1·v2 = Σ(v1_i * v2_i)
    dot_product = np.dot(v1, v2)
    print(f"\nv1 · v2 = {dot_product}")

    # 使用內積計算向量間的夾角
    # cos(θ) = (v1 · v2) / (||v1|| * ||v2||)
    norm_v2 = np.linalg.norm(v2)
    cos_angle = dot_product / (norm_v1 * norm_v2)
    angle_rad = np.arccos(cos_angle)
    angle_deg = np.degrees(angle_rad)
    print(f"v1 與 v2 的夾角: {angle_deg:.2f} 度")

    # 投影
    # v1 在 v2 上的投影向量
    projection = (dot_product / np.dot(v2, v2)) * v2
    print(f"v1 在 v2 上的投影: {projection}")

def demonstrate_3d_vectors():
    """
    展示三維向量的特殊運算
    """
    print("\n三維向量運算")
    print("=" * 40)

    # 建立三維向量
    a = np.array([1, 0, 0])  # x 軸方向
    b = np.array([0, 1, 0])  # y 軸方向

    print(f"向量 a: {a}")
    print(f"向量 b: {b}")

    # 外積(叉積)
    # 只對三維向量定義
    # 結果是垂直於兩個輸入向量的向量
    cross_product = np.cross(a, b)
    print(f"\na × b = {cross_product}")

    # 驗證垂直性
    # 叉積結果與原向量的點積應為零
    print(f"(a × b) · a = {np.dot(cross_product, a)}")
    print(f"(a × b) · b = {np.dot(cross_product, b)}")

    # 計算平行四邊形面積
    # 面積 = ||a × b||
    area = np.linalg.norm(cross_product)
    print(f"由 a 和 b 構成的平行四邊形面積: {area}")

# 執行示範
if __name__ == "__main__":
    demonstrate_vector_operations()
    demonstrate_3d_vectors()

這些向量運算在資料科學中有廣泛的應用。內積用於計算相似度(如餘弦相似度)、投影用於降維分析(如 PCA)、範數用於正則化和距離計算。理解這些基本運算是學習更進階技術的基礎。

矩陣運算與線性變換

矩陣是線性代數的核心對象,它可以表示線性變換、儲存資料,或表達方程組。矩陣運算包括加法、乘法、轉置、求逆等,每種運算都有其數學意義和計算規則。

import numpy as np

def demonstrate_matrix_operations():
    """
    展示矩陣的基本運算
    """
    print("矩陣運算示範")
    print("=" * 40)

    # 建立矩陣
    # 二維 NumPy 陣列表示矩陣
    A = np.array([
        [1, 2, 3],
        [4, 5, 6]
    ])  # 2x3 矩陣

    B = np.array([
        [7, 8],
        [9, 10],
        [11, 12]
    ])  # 3x2 矩陣

    print(f"矩陣 A ({A.shape[0]}x{A.shape[1]}):")
    print(A)
    print(f"\n矩陣 B ({B.shape[0]}x{B.shape[1]}):")
    print(B)

    # 矩陣乘法
    # A 的列數必須等於 B 的行數
    # 結果矩陣的維度是 (A的行數 x B的列數)
    C = np.matmul(A, B)  # 或 A @ B
    print(f"\nA × B = ({C.shape[0]}x{C.shape[1]}):")
    print(C)

    # 矩陣轉置
    # 行列互換
    A_T = A.T
    print(f"\nA 的轉置:")
    print(A_T)

    # 元素運算
    # 這些是逐元素的運算,不是矩陣運算
    D = np.array([
        [1, 2],
        [3, 4]
    ])

    E = np.array([
        [5, 6],
        [7, 8]
    ])

    print("\n方陣運算")
    print(f"矩陣 D:\n{D}")
    print(f"矩陣 E:\n{E}")

    # 元素相乘(Hadamard product)
    hadamard = D * E
    print(f"\nD ⊙ E (元素相乘):\n{hadamard}")

    # 矩陣乘法
    matrix_product = D @ E
    print(f"\nD × E (矩陣乘法):\n{matrix_product}")

def demonstrate_special_matrices():
    """
    展示特殊矩陣和相關運算
    """
    print("\n特殊矩陣與進階運算")
    print("=" * 40)

    # 單位矩陣
    # 對角線為 1,其餘為 0
    I = np.eye(3)
    print("3x3 單位矩陣:")
    print(I)

    # 對角矩陣
    diag_values = [1, 2, 3]
    D = np.diag(diag_values)
    print(f"\n對角矩陣 (對角元素: {diag_values}):")
    print(D)

    # 矩陣的行列式
    # 只對方陣定義
    A = np.array([
        [3, 1],
        [2, 4]
    ])
    det_A = np.linalg.det(A)
    print(f"\n矩陣 A:\n{A}")
    print(f"det(A) = {det_A:.2f}")

    # 矩陣的逆
    # 只有行列式非零的方陣才可逆
    if det_A != 0:
        A_inv = np.linalg.inv(A)
        print(f"\nA 的逆矩陣:")
        print(A_inv)

        # 驗證:A × A⁻¹ = I
        product = A @ A_inv
        print(f"\nA × A⁻¹:")
        print(np.round(product, 10))  # 四捨五入以處理浮點誤差

    # 矩陣的跡(trace)
    # 對角元素之和
    trace_A = np.trace(A)
    print(f"\ntr(A) = {trace_A}")

    # 矩陣的秩(rank)
    # 線性獨立的行(或列)的最大數量
    rank_A = np.linalg.matrix_rank(A)
    print(f"rank(A) = {rank_A}")

def solve_linear_system():
    """
    使用矩陣求解線性方程組
    """
    print("\n線性方程組求解")
    print("=" * 40)

    # 方程組:
    # 2x + y = 5
    # x + 3y = 5

    # 係數矩陣
    A = np.array([
        [2, 1],
        [1, 3]
    ])

    # 常數向量
    b = np.array([5, 5])

    print("方程組:")
    print("2x + y = 5")
    print("x + 3y = 5")

    # 方法 1:使用逆矩陣
    # x = A⁻¹b
    x_inv = np.linalg.inv(A) @ b
    print(f"\n使用逆矩陣求解: x = {x_inv}")

    # 方法 2:使用 solve 函式(數值穩定性更好)
    x_solve = np.linalg.solve(A, b)
    print(f"使用 solve 函式: x = {x_solve}")

    # 驗證解
    residual = A @ x_solve - b
    print(f"\n驗證 (Ax - b): {residual}")

# 執行示範
if __name__ == "__main__":
    demonstrate_matrix_operations()
    demonstrate_special_matrices()
    solve_linear_system()

矩陣運算在機器學習中無處不在。線性迴歸的正規方程式、神經網路的前向傳播和反向傳播、主成分分析的特徵分解,都依賴於這些基本的矩陣運算。NumPy 利用底層的 BLAS 和 LAPACK 函式庫,提供了高效能的實作。

幾何變換與旋轉矩陣

線性變換可以透過矩陣來表示,這在電腦圖形學、機器人學和物理模擬中有重要應用。常見的幾何變換包括旋轉、縮放、剪切和反射。理解這些變換的矩陣表示,有助於處理各種空間變換問題。

旋轉矩陣是最常用的幾何變換之一。在二維空間中,旋轉矩陣將向量繞原點旋轉指定的角度。旋轉矩陣是正交矩陣(其轉置等於其逆),這意味著它保持向量的長度和向量間的夾角。

import numpy as np

def rotation_matrix_2d(theta: float) -> np.ndarray:
    """
    建立二維旋轉矩陣

    旋轉矩陣的形式:
    R(θ) = [cos(θ)  -sin(θ)]
           [sin(θ)   cos(θ)]

    這個矩陣將向量逆時針旋轉角度 θ

    Args:
        theta: 旋轉角度(弧度)

    Returns:
        2x2 旋轉矩陣
    """
    cos_t = np.cos(theta)
    sin_t = np.sin(theta)

    return np.array([
        [cos_t, -sin_t],
        [sin_t,  cos_t]
    ])

def rotation_matrix_3d(axis: str, theta: float) -> np.ndarray:
    """
    建立三維旋轉矩陣

    三維空間中的旋轉需要指定旋轉軸

    Args:
        axis: 旋轉軸 ('x', 'y', 'z')
        theta: 旋轉角度(弧度)

    Returns:
        3x3 旋轉矩陣
    """
    cos_t = np.cos(theta)
    sin_t = np.sin(theta)

    if axis == 'x':
        # 繞 x 軸旋轉
        return np.array([
            [1, 0, 0],
            [0, cos_t, -sin_t],
            [0, sin_t, cos_t]
        ])
    elif axis == 'y':
        # 繞 y 軸旋轉
        return np.array([
            [cos_t, 0, sin_t],
            [0, 1, 0],
            [-sin_t, 0, cos_t]
        ])
    elif axis == 'z':
        # 繞 z 軸旋轉
        return np.array([
            [cos_t, -sin_t, 0],
            [sin_t, cos_t, 0],
            [0, 0, 1]
        ])
    else:
        raise ValueError(f"無效的旋轉軸: {axis}")

def demonstrate_rotation():
    """
    展示旋轉變換的應用
    """
    print("旋轉變換示範")
    print("=" * 40)

    # 二維旋轉
    # 將向量旋轉 90 度(π/2 弧度)
    theta = np.pi / 2
    R = rotation_matrix_2d(theta)

    # 原始向量指向 x 軸正方向
    v = np.array([1, 0])

    # 旋轉後的向量
    v_rotated = R @ v

    print("二維旋轉:")
    print(f"  原始向量: {v}")
    print(f"  旋轉角度: {np.degrees(theta):.0f} 度")
    print(f"  旋轉後: {np.round(v_rotated, 10)}")

    # 驗證旋轉矩陣的性質
    print("\n旋轉矩陣性質驗證:")

    # 正交性:R^T = R^(-1)
    R_inv = np.linalg.inv(R)
    R_T = R.T
    print(f"  R^T 近似等於 R^(-1): {np.allclose(R_T, R_inv)}")

    # 行列式為 1
    det_R = np.linalg.det(R)
    print(f"  det(R) = {det_R:.0f}")

    # 保持向量長度
    norm_original = np.linalg.norm(v)
    norm_rotated = np.linalg.norm(v_rotated)
    print(f"  原始長度: {norm_original}, 旋轉後長度: {norm_rotated:.10f}")

    # 三維旋轉
    print("\n三維旋轉:")

    # 繞 z 軸旋轉 45 度
    theta_3d = np.pi / 4
    R_z = rotation_matrix_3d('z', theta_3d)

    v_3d = np.array([1, 0, 0])
    v_3d_rotated = R_z @ v_3d

    print(f"  原始向量: {v_3d}")
    print(f"  繞 z 軸旋轉 {np.degrees(theta_3d):.0f} 度")
    print(f"  旋轉後: {np.round(v_3d_rotated, 4)}")

def demonstrate_other_transforms():
    """
    展示其他常見的幾何變換
    """
    print("\n其他幾何變換")
    print("=" * 40)

    # 縮放矩陣
    # 在 x 和 y 方向分別縮放 2 倍和 0.5 倍
    scale_x, scale_y = 2.0, 0.5
    S = np.array([
        [scale_x, 0],
        [0, scale_y]
    ])

    v = np.array([1, 1])
    v_scaled = S @ v

    print("縮放變換:")
    print(f"  原始向量: {v}")
    print(f"  縮放因子: x={scale_x}, y={scale_y}")
    print(f"  縮放後: {v_scaled}")

    # 剪切矩陣
    # x 方向的剪切
    shear = 0.5
    Sh = np.array([
        [1, shear],
        [0, 1]
    ])

    v_sheared = Sh @ v

    print("\n剪切變換:")
    print(f"  原始向量: {v}")
    print(f"  剪切因子: {shear}")
    print(f"  剪切後: {v_sheared}")

    # 反射矩陣
    # 關於 x 軸的反射
    Ref_x = np.array([
        [1, 0],
        [0, -1]
    ])

    v_test = np.array([1, 2])
    v_reflected = Ref_x @ v_test

    print("\n反射變換:")
    print(f"  原始向量: {v_test}")
    print(f"  關於 x 軸反射後: {v_reflected}")

# 執行示範
if __name__ == "__main__":
    demonstrate_rotation()
    demonstrate_other_transforms()

以下是常見幾何變換的矩陣形式示意圖:

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "二維線性變換" {

    class "旋轉 R(θ)" as rotation {
        [cos(θ)  -sin(θ)]
        [sin(θ)   cos(θ)]
        --
        保持長度
        保持夾角
        det(R) = 1
    }

    class "縮放 S" as scaling {
        [sx   0]
        [0   sy]
        --
        改變長度
        不改變方向
        det(S) = sx*sy
    }

    class "剪切 Sh" as shear {
        [1   k]
        [0   1]
        --
        保持面積
        改變形狀
        det(Sh) = 1
    }

    class "反射 Ref" as reflection {
        關於 x 軸:
        [1    0]
        [0   -1]
        --
        改變方向性
        det(Ref) = -1
    }
}

note bottom of rotation
  旋轉是正交變換
  R^T = R^(-1)
end note

note bottom of scaling
  特殊情況:
  - sx = sy 等比縮放
  - sx = -1 反射
end note

@enduml

特徵值分解與主成分分析基礎

特徵值和特徵向量是理解矩陣本質的關鍵概念。對於一個方陣 A,如果存在非零向量 v 和純量 λ,使得 Av = λv,則 λ 是 A 的特徵值,v 是對應的特徵向量。特徵分解將矩陣分解為特徵向量和特徵值的組合,這在主成分分析(PCA)、譜聚類等演算法中有重要應用。

import numpy as np

def demonstrate_eigendecomposition():
    """
    展示特徵值分解及其應用
    """
    print("特徵值分解")
    print("=" * 40)

    # 建立一個對稱矩陣
    # 對稱矩陣的特徵值都是實數,特徵向量彼此正交
    A = np.array([
        [4, 2],
        [2, 3]
    ])

    print(f"矩陣 A:\n{A}")

    # 計算特徵值和特徵向量
    eigenvalues, eigenvectors = np.linalg.eig(A)

    print(f"\n特徵值: {eigenvalues}")
    print(f"\n特徵向量:\n{eigenvectors}")

    # 驗證特徵方程 Av = λv
    print("\n驗證特徵方程 Av = λv:")
    for i in range(len(eigenvalues)):
        lam = eigenvalues[i]
        v = eigenvectors[:, i]

        # 計算 Av 和 λv
        Av = A @ v
        lambda_v = lam * v

        print(f"  λ_{i+1} = {lam:.4f}")
        print(f"  v_{i+1} = {v}")
        print(f"  Av = {Av}")
        print(f"  λv = {lambda_v}")
        print(f"  相等: {np.allclose(Av, lambda_v)}\n")

    # 使用特徵分解重建矩陣
    # A = VΛV^(-1),其中 V 是特徵向量矩陣,Λ 是特徵值對角矩陣
    V = eigenvectors
    Lambda = np.diag(eigenvalues)
    V_inv = np.linalg.inv(V)

    A_reconstructed = V @ Lambda @ V_inv
    print(f"重建的矩陣:\n{np.round(A_reconstructed, 10)}")
    print(f"重建成功: {np.allclose(A, A_reconstructed)}")

def simple_pca_demo():
    """
    展示主成分分析的基本概念
    """
    print("\n主成分分析基礎")
    print("=" * 40)

    # 生成二維資料
    # 兩個變數有正相關
    np.random.seed(42)
    n = 100

    # 生成相關的資料
    mean = [0, 0]
    cov = [[1, 0.8], [0.8, 1]]  # 共變異數矩陣,相關係數 0.8
    data = np.random.multivariate_normal(mean, cov, n)

    print(f"資料維度: {data.shape}")
    print(f"資料均值: {data.mean(axis=0)}")

    # 步驟 1:中心化資料
    # 減去均值使資料以原點為中心
    data_centered = data - data.mean(axis=0)

    # 步驟 2:計算共變異數矩陣
    cov_matrix = np.cov(data_centered.T)
    print(f"\n共變異數矩陣:\n{cov_matrix}")

    # 步驟 3:計算特徵值和特徵向量
    eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)

    # 按特徵值大小排序
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    print(f"\n特徵值(降序): {eigenvalues}")
    print(f"\n主成分方向:\n{eigenvectors}")

    # 計算解釋變異比例
    total_var = eigenvalues.sum()
    explained_var_ratio = eigenvalues / total_var
    cumulative_var_ratio = np.cumsum(explained_var_ratio)

    print("\n解釋變異:")
    for i, (var_ratio, cum_ratio) in enumerate(zip(explained_var_ratio, cumulative_var_ratio)):
        print(f"  PC{i+1}: {var_ratio*100:.2f}% (累積: {cum_ratio*100:.2f}%)")

    # 步驟 4:投影資料到主成分
    # 將資料投影到第一主成分
    pc1 = eigenvectors[:, 0]
    projected = data_centered @ pc1

    print(f"\n投影到第一主成分後的資料維度: {projected.shape}")
    print(f"投影資料的變異數: {projected.var():.4f}")
    print(f"第一主成分的特徵值: {eigenvalues[0]:.4f}")

    return data, eigenvectors, eigenvalues

# 執行示範
if __name__ == "__main__":
    demonstrate_eigendecomposition()
    data, eigenvectors, eigenvalues = simple_pca_demo()

主成分分析(PCA)是一種降維技術,它找出資料中變異最大的方向。這些方向就是共變異數矩陣的特徵向量,對應的特徵值表示該方向上的變異量。透過保留最大的幾個特徵值對應的主成分,可以在保留大部分資訊的同時減少資料的維度。

Pandas DataFrame 的矩陣操作

Pandas DataFrame 可以視為帶有標籤的矩陣,它結合了 NumPy 的數值計算能力和資料庫的資料管理功能。理解 DataFrame 與矩陣的關係,可以更有效地利用 Pandas 進行資料分析。

import pandas as pd
import numpy as np

def demonstrate_dataframe_matrix_operations():
    """
    展示 DataFrame 的矩陣運算
    """
    print("DataFrame 矩陣運算")
    print("=" * 40)

    # 建立 DataFrame
    # DataFrame 可以視為帶有行列標籤的矩陣
    df = pd.DataFrame({
        'A': [1, 2, 3],
        'B': [4, 5, 6],
        'C': [7, 8, 9]
    }, index=['x', 'y', 'z'])

    print("原始 DataFrame:")
    print(df)

    # 取得底層的 NumPy 陣列
    matrix = df.values  # 或 df.to_numpy()
    print(f"\n底層矩陣:\n{matrix}")
    print(f"型別: {type(matrix)}")

    # 矩陣轉置
    # DataFrame 的 T 屬性返回轉置的 DataFrame
    df_T = df.T
    print(f"\n轉置:\n{df_T}")

    # 矩陣乘法
    # 使用 @ 運算符或 dot 方法
    df1 = pd.DataFrame({
        'a': [1, 2],
        'b': [3, 4]
    })

    df2 = pd.DataFrame({
        'x': [5, 6],
        'y': [7, 8]
    })

    result = df1 @ df2.values  # 將 df2 轉換為陣列以進行矩陣乘法
    print(f"\n矩陣乘法結果:\n{result}")

    # 協方差矩陣
    # cov() 方法計算 DataFrame 各欄位間的協方差
    cov_matrix = df.cov()
    print(f"\n協方差矩陣:\n{cov_matrix}")

    # 相關係數矩陣
    # corr() 方法計算 Pearson 相關係數
    corr_matrix = df.corr()
    print(f"\n相關係數矩陣:\n{corr_matrix}")

def analyze_stock_correlations():
    """
    使用 DataFrame 分析股票相關性
    """
    print("\n股票相關性分析")
    print("=" * 40)

    # 生成模擬的股票報酬率資料
    np.random.seed(42)
    n_days = 100

    # 模擬三檔股票的報酬率
    # 設定不同的相關性
    base_returns = np.random.normal(0, 0.02, n_days)

    returns = pd.DataFrame({
        'TECH': base_returns + np.random.normal(0, 0.01, n_days),
        'BANK': base_returns * 0.5 + np.random.normal(0, 0.015, n_days),
        'UTIL': np.random.normal(0.001, 0.01, n_days)  # 較不相關
    })

    print("報酬率統計:")
    print(returns.describe())

    # 計算相關係數矩陣
    corr = returns.corr()
    print(f"\n相關係數矩陣:\n{corr}")

    # 計算協方差矩陣
    cov = returns.cov()
    print(f"\n協方差矩陣:\n{cov}")

    # 使用特徵分解分析風險結構
    eigenvalues, eigenvectors = np.linalg.eig(cov.values)

    # 排序
    idx = eigenvalues.argsort()[::-1]
    eigenvalues = eigenvalues[idx]
    eigenvectors = eigenvectors[:, idx]

    print("\n風險結構分析 (主成分):")
    total_var = eigenvalues.sum()
    for i, val in enumerate(eigenvalues):
        print(f"  PC{i+1}: 變異數 = {val:.6f}, 解釋比例 = {val/total_var*100:.1f}%")

    # 主成分載荷
    loadings = pd.DataFrame(
        eigenvectors,
        index=returns.columns,
        columns=[f'PC{i+1}' for i in range(len(eigenvalues))]
    )
    print(f"\n主成分載荷:\n{loadings}")

    return returns, corr, cov

# 執行示範
if __name__ == "__main__":
    demonstrate_dataframe_matrix_operations()
    returns, corr, cov = analyze_stock_correlations()

這個範例展示了如何將線性代數的概念應用於實際的資料分析。相關係數矩陣是標準化後的協方差矩陣,它提供了變數間線性關係的度量。主成分分析可以揭示資料中的隱藏結構,例如在股票報酬率中識別共同的風險因子。

資料科學工作流程的最佳實踐

在結束本文之前,值得總結一些資料科學工作流程中的最佳實踐。這些實踐不僅能提高工作效率,也能確保分析結果的可重現性和可靠性。

首先是資料處理的管線化。將資料處理步驟組織成清晰的管線,每個步驟都有明確的輸入和輸出。這樣做可以方便調試、測試和修改。Pandas 的方法鏈(method chaining)是實現管線化的好方法。

其次是型別檢查和資料驗證。在處理資料之前,驗證資料的型別、範圍和完整性。及早發現資料問題可以避免在分析後期遇到難以追蹤的錯誤。

第三是向量化運算。盡量使用 NumPy 和 Pandas 的向量化操作,而不是 Python 的迴圈。向量化操作利用底層的優化庫,速度可以快上百倍。

第四是隨機種子的控制。在涉及隨機操作的程式碼中,設定隨機種子以確保結果可重現。這對於調試和報告都很重要。

最後是文件和註解。清晰的文件和註解不僅幫助他人理解你的程式碼,也幫助未來的自己。特別是對於複雜的數學運算,說明所用的公式和假設是非常必要的。

透過掌握這些 Python 資料科學基礎和線性代數知識,讀者將能夠更自信地進行資料分析工作,並為學習更進階的機器學習技術打下堅實的基礎。Python 和線性代數的結合,提供了一個強大而靈活的工具集,能夠處理從簡單的數據清理到複雜的統計建模的各種任務。