返回目錄
A
數據驅動的投資分析:從基礎到實戰 - 第 3 章
第 3 章 數據清洗與前處理:確保分析品質
發布於 2026-02-27 08:24
# 第 3 章 數據清洗與前處理:確保分析品質
在前兩章我們已經了解了資料來源與結構,接下來要進行的是「把雜亂的原始資料轉換成乾淨、可直接投入模型的格式」。這一步驟常被稱為 **ETL**(Extract‑Transform‑Load),是投資分析流程中最關鍵也是最容易出錯的環節。資料質量不佳會直接影響模型的預測準確度,甚至導致錯誤的投資決策。以下從概念、技術、實務範例三個層面說明。
## 3.1 什麼是資料清洗?
| 步驟 | 目的 | 典型作業 |
|------|------|----------|
| 缺失值處理 | 消除資料的不完整性,避免模型崩潰 | 0 值填補、前向/後向填充、插值 |
| 重複資料剔除 | 避免統計量被重複計算,保持樣本獨立性 | `drop_duplicates()` |
| 异常值偵測 | 確保統計量不被極端值扭曲 | IQR、Z‑score、DBSCAN |
| 資料型別轉換 | 讓演算法能正確解讀 | 數值→浮點、時間→datetime |
| 時間序列同步 | 同一時間維度,避免資訊不對稱 | 重新取樣、前向填補 |
| 統一單位與尺度 | 方便因子合併與模型比較 | 調整面積、價格、比例 |
> **核心觀點**:資料清洗不是「刪除一切」,而是「保留對分析有價值、可解讀的資訊」。
## 3.2 缺失值處理
### 3.2.1 失效的原因
- 資料來源不完整(API 限制、權限不足)
- 交易停盤、公司停牌、公告缺漏
- 數據抓取過程中的錯誤或衝突
### 3.2.2 常見策略
| 方法 | 適用情境 | 優缺點 |
|------|----------|--------|
| 刪除行/列 | 缺失比例低於 5% | 可能造成樣本量減少 |
| 0 值填補 | 連續數值、指標缺失 | 可能低估波動率 |
| 前向/後向填充 | 時間序列缺失 | 可能保留舊資訊 |
| 均值/中位數填補 | 統計學方法 | 可能忽略時間趨勢 |
| 迴歸插值 | 需保留趨勢 | 計算成本較高 |
| KNN/LOF | 多變量缺失 | 需要參數調整 |
### 3.2.3 範例:Python pandas
python
import pandas as pd
# 讀取範例資料
df = pd.read_csv('stock_prices.csv', parse_dates=['date'])
# 1. 低比例缺失直接刪除
df = df.dropna(thresh=int(0.95*len(df)))
# 2. 連續日期填補(重取樣)
df.set_index('date', inplace=True)
df = df.resample('D').asfreq()
# 3. 前向填充缺失價格
df['close'] = df['close'].fillna(method='ffill')
# 4. 仍缺失的行可使用均值填補
df['volume'] = df['volume'].fillna(df['volume'].median())
> **小提醒**:在填補缺失值前,先確認該欄位為 **時間連續**,否則前向/後向填補可能產生錯誤邏輯。
## 3.3 重複資料剔除
> **情境**:多來源合併、API 重複抓取。
python
# 以 (date, ticker) 為唯一鍵
df = df.drop_duplicates(subset=['date', 'ticker'], keep='last')
### 重複檢查技巧
- **行雜湊**:計算整行哈希值,快速檢測是否全列相同。
- **子集檢查**:僅關注關鍵欄位(如 `open`, `close`, `volume`)作比較。
## 3.4 异常值偵測與處理
| 方法 | 公式 | 適用範例 |
|------|------|----------|
| IQR | Q3‑Q1 | 交易量異常大幅跳變 |
| Z‑score | (x‑μ)/σ | 價格劇烈波動 |
| DBSCAN | eps, min_samples | 連續異常點聚類 |
python
# IQR 方法
Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
# 篩除異常
df = df[(df['close'] >= lower) & (df['close'] <= upper)]
> **小技巧**:在金融資料中,某些「異常」實際上可能是事件驅動(如重大公告),不應盲目剔除。建議先加上事件標籤,再決定處理方式。
## 3.5 資料型別與單位轉換
| 欄位 | 原型別 | 轉換後型別 | 備註 |
|------|--------|------------|------|
| price | object | float64 | 轉換後可計算收益 |
| date | object | datetime64[ns] | 方便時間序列操作 |
| volume | str | int64 | 去除逗號、空格 |
| currency | object | str | 轉換成統一貨幣(USD) |
> **注意**:統一單位前務必確認來源與匯率來源。若使用多個國家資料,匯率波動可能對模型產生系統性誤差。
## 3.6 時間序列同步
### 3.6.1 重新取樣(Resampling)
| 頻率 | 描述 |
|------|------|
| `D` | 日頻 |
| `W` | 周頻 |
| `M` | 月頻 |
python
# 以日頻為例
# 先將索引設定為日期
df.set_index('date', inplace=True)
# 重新取樣,計算收盤價平均
df_daily = df['close'].resample('D').last()
### 3.6.2 前向填充(Forward Fill)
當交易所停盤或資料缺失時,使用前一個有效值填補。
python
df['close'] = df['close'].fillna(method='ffill')
> **實務提醒**:若前向填補時間跨度超過 5 個工作日,建議重新評估是否應剔除該段資料,避免帶入不實訊息。
## 3.7 統一資料格式(ETL Pipeline 範例)
python
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from datetime import datetime
# 1. Extract
def extract(**kwargs):
# 讀取 CSV / API
return pd.read_csv('raw_stock_data.csv', parse_dates=['date'])
# 2. Transform
def transform(**kwargs):
df = kwargs['ti'].xcom_pull(task_ids='extract')
# ① 缺失值處理
df = df.dropna(subset=['close', 'volume'])
# ② 重複剔除
df = df.drop_duplicates(subset=['date', 'ticker'], keep='last')
# ③ 异常值檢測
Q1, Q3 = df['close'].quantile([0.25, 0.75])
IQR = Q3 - Q1
df = df[(df['close'] >= Q1-1.5*IQR) & (df['close'] <= Q3+1.5*IQR)]
# ④ 資料型別轉換
df['close'] = df['close'].astype(float)
df['volume'] = df['volume'].astype(int)
df.set_index('date', inplace=True)
df = df.resample('D').last()
df['close'] = df['close'].fillna(method='ffill')
df.reset_index(inplace=True)
return df
# 3. Load
def load(**kwargs):
df = kwargs['ti'].xcom_pull(task_ids='transform')
df.to_sql('clean_stock_prices', con=engine, if_exists='replace', index=False)
# DAG 定義
with DAG(
dag_id='stock_etl',
start_date=datetime(2023, 1, 1),
schedule_interval='@daily',
catchup=False
) as dag:
t_extract = PythonOperator(task_id='extract', python_callable=extract, provide_context=True)
t_transform = PythonOperator(task_id='transform', python_callable=transform, provide_context=True)
t_load = PythonOperator(task_id='load', python_callable=load, provide_context=True)
t_extract >> t_transform >> t_load
> **結語**:資料清洗不是一次性工作,而是資料循環更新、模型再訓練時需要持續重複的作業。建議將清洗流程封裝成自動化 Pipeline,確保每次載入資料都能遵循同樣的規範。
## 3.8 小結
1. **缺失值**:選擇合適填補方法,並根據缺失比例決定刪除或保留。
2. **重複資料**:以關鍵鍵值(日期+ticker)為基準,避免重複計算。
3. **异常值**:先加上事件標籤,慎重剔除。
4. **型別 & 單位**:統一型別後可直接投入模型,避免演算法崩潰。
5. **時間同步**:重新取樣 + 前向填補,確保每個觀測都有對應的時間戳。
完成這些步驟後,我們得到的「乾淨資料」即可直接輸入回歸、時間序列預測或機器學習模型。下章將把這份資料帶入 **因子構建** 與 **模型訓練**,繼續深化投資策略。