跳到主要內容

DE Day 35 端到端管線(五):效能與成本

DE Day 35 端到端管線(五):效能與成本

執行需求:CPU 可跑。今天是端到端管線系列的第五天,也是這個專案篇章的最後一篇。我們要用 EXPLAIN ANALYZE 比較 CSV 與 Parquet 的查詢時間、討論分區合併策略、計算管線的 CPU 時間成本,並用一個可以整段執行的範例展示「為什麼 Parquet 對大資料集更划算」。本篇所有範例都在 CPU 上執行。讀完這篇,你會對這條管線的效能特性有具體數字的概念,並能在部署到雲端時估算每月成本。

引言

前四天我們把管線建起來了:採集、轉換、品質、通知、展示。今天要回答一個很多資料工程團隊會忽略的問題:「這條管線到底要花多少錢?」效能與成本不只是「把程式改快一點」,更是「用多少機器跑得起來」。如果不知道成本,我們就沒辦法決定要部署到哪個等級的雲端機器;如果不知道效能特性,我們就沒辦法告訴使用者「這份報表要等多久」。

本章的目標有兩個:第一,用 EXPLAIN ANALYZE 量化比較 CSV、Parquet、DuckDB 三種儲存方式的查詢時間差異;第二,建立一個簡單的成本估算模型,把「管線跑一次的 CPU 時間」轉成「每月雲端費用」。我們會用具體的數字(70 萬筆公司資料、9 萬筆變更事件、每天跑一次)來做估算,並把數字整理成一張表方便日後對照。

效能與成本的核心觀念

資料工程的效能瓶頸通常不在 Python 程式碼,而在「磁碟 I/O」與「資料掃描範圍」。以我們的管線為例,70 萬筆的 staging.company_basic_clean 從頭掃到尾需要 0.3 秒(純記憶體運算);但如果同樣的查詢從 CSV 讀,磁碟 I/O 會把時間拉到 1.5 秒;如果從 Parquet 讀(zstd 壓縮),時間會在 0.5 秒左右。這些數字看起來都很小,但當資料量放大 100 倍(7000 萬筆),差距就會從「1 秒 vs 0.3 秒」變成「100 秒 vs 30 秒」——這時選對儲存格式就很重要了。

另一個關鍵觀念是「分區的代價」。Day 30 介紹的分區(每天一個資料夾)對增量載入很方便,但對查詢來說是「額外的檔案數」。如果一年累積 365 個 Parquet 檔案,每次查詢要開啟 365 個檔案就會有效能問題。解法是「分區合併(compaction)」:定期把小檔案合併成大檔案。實務上我們會建議「每季合併一次」,把一季 90 個檔案合併成一個大檔案;同時用 read_parquet(..., hive_partitioning=true) 讓 DuckDB 自動偵測分區。

成本估算方面,雲端機器的計價方式分為「依執行時間」與「依資源大小」。AWS Lambda 與 Google Cloud Run 是前者(每 100ms 計費一次),EC2 與 Cloud SQL 是後者(每月固定費率)。本系列的管線每次跑約 5 分鐘、每天跑一次、月跑 30 次,如果用 Lambda 計價,每月約 2.5 小時 × $0.000016/秒 ≈ $0.14 美元;如果用一台 2 vCPU 的 EC2($0.04/小時)整天開著跑,每月 $29 美元。差異主要看「是否要整天開機接收其他任務」。我們會在今天的範例中展示一個簡單的成本估算腳本。

共用設定:效能與成本常數

本篇不需要大幅擴充 common.py,只需要新增兩個常數:

"""de-journey/pipelines/common.py:Day 30-35 共用的管線常數。"""
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parents[1]
DATA_DIR = PROJECT_ROOT / "data"
WAREHOUSE_DIR = PROJECT_ROOT / "warehouse"
LOGS_DIR = PROJECT_ROOT / "logs"
NOTIFICATIONS_DIR = LOGS_DIR / "notifications"

DUCKDB_PATH = WAREHOUSE_DIR / "de-journey.duckdb"

DATASETS = {
    "company_basic": {
        "title": "公司登記基本資料",
        "source": "moea_basic",
        "table": "raw.company_basic",
        "staging_table": "staging.company_basic_clean",
        "dim_table": "mart.dim_company",
        "key_columns": ["uniform_no", "company_name"],
        "partition_prefix": "company_basic",
    },
    "company_change": {
        "title": "公司變更登記資料",
        "source": "moea_change",
        "table": "raw.company_change",
        "staging_table": "staging.company_change_clean",
        "fact_table": "mart.fact_company_change",
        "key_columns": ["uniform_no", "change_date", "change_item"],
        "partition_prefix": "company_change",
    },
}

# 效能與成本(Day 35)

COST_ESTIMATES = {
    # 每個階段的預估 CPU 時間(秒)

    "ingest_sec": 60,
    "transform_sec": 30,
    "build_marts_sec": 15,
    "check_quality_sec": 10,
    # 雲端成本(美元/小時,2 vCPU 為基準)

    "hourly_rate_usd": 0.04,
}

QUALITY_RULES = {
    "company_basic": [
        ("uniform_no_not_null", "uniform_no IS NOT NULL", "critical", 0),
        ("uniform_no_length_8", "LENGTH(uniform_no) = 8", "critical", 0),
        ("company_name_not_null", "company_name IS NOT NULL", "critical", 0),
        ("company_name_min_len", "LENGTH(company_name) >= 2", "warning", 100),
        ("capital_amount_non_negative", "capital_amount >= 0", "critical", 0),
        ("establish_date_valid", "establish_date IS NOT NULL", "warning", 1000),
    ],
    "company_change": [
        ("uniform_no_not_null", "uniform_no IS NOT NULL", "critical", 0),
        ("change_date_not_null", "change_date IS NOT NULL", "critical", 0),
        ("change_item_not_null", "change_item IS NOT NULL", "critical", 0),
    ],
}

NOTIFY_CONFIG = {
    "recipients": ["data-eng@example.invalid"],
    "subject_template": "[DE Pipeline] {dataset} 品質異常 {date}",
    "is_simulation": True,
}

MIN_DAILY_ROW_RATIO = 0.90
HTTP_TIMEOUT_SEC = 30
RETRY_ATTEMPTS = 3
RETRY_BACKOFF_SEC = 2.0

這份擴充重點是新增 COST_ESTIMATES 字典,把每個階段的 CPU 時間與雲端時薪寫成常數。這樣日後調整(例如實際測量後發現 transform 比較慢)只要改一處。

完整實作:用 EXPLAIN ANALYZE 比較儲存格式

先用 EXPLAIN ANALYZE 量化比較同一個查詢在三種儲存上的執行時間。先建一份測試資料:

"""de-journey/pipelines/benchmark_storage.py:比較 CSV vs Parquet vs DuckDB table。"""
import shutil
import tempfile
import time
from pathlib import Path

import duckdb

from pipelines.common import DUCKDB_PATH, DATA_DIR

# 把一份測試資料輸出到三種格式

con = duckdb.connect(str(DUCKDB_PATH))
csv_path = Path(tempfile.mkdtemp()) / "benchmark.csv"
parquet_path = csv_path.with_suffix(".parquet")
con.execute(f"COPY (SELECT * FROM staging.company_basic_clean LIMIT 500000) TO '{csv_path}' (FORMAT CSV, HEADER)")
con.execute(f"COPY (SELECT * FROM staging.company_basic_clean LIMIT 500000) TO '{parquet_path}' (FORMAT PARQUET, COMPRESSION zstd)")

# 三種儲存:A. CSV 直讀;B. Parquet 直讀;C. 從 DuckDB 表讀

sql_template = """
    SELECT COUNT(*) AS n, AVG(capital_amount) AS avg_cap
    FROM {source}
"""
sources = [
    ("CSV", f"read_csv_auto('{csv_path}')"),
    ("Parquet", f"read_parquet('{parquet_path}')"),
    ("DuckDB table", "staging.company_basic_clean"),
]

for label, src in sources:
    sql = sql_template.format(source=src)
    # 暖機一次避免冷啟動影響

    con.execute(sql).fetchall()
    # 正式計時(跑 5 次取平均)

    times = []
    for _ in range(5):
        t0 = time.perf_counter()
        con.execute(sql).fetchall()
        times.append(time.perf_counter() - t0)
    avg = sum(times) / len(times)
    print(f"{label}: 平均 {avg*1000:.1f} ms")  # 輸出:CSV 1450 ms、Parquet 480 ms、DuckDB 320 ms

這支腳本先複製 50 萬筆資料到 CSV 與 Parquet,再對三種儲存各跑 5 次相同的聚合查詢(COUNT(*) 與 AVG(capital_amount))。第一次跑是「暖機」——把資料讀進 DuckDB 的快取——避免冷啟動的時間污染;接下來 5 次取平均才是實際的查詢時間。在普通筆電上,輸出大致是:CSV 約 1.5 秒、Parquet 約 0.5 秒、DuckDB table 約 0.3 秒。差距 5 倍。

進一步用 EXPLAIN ANALYZE 看 DuckDB 內部的執行計畫:

"""de-journey/pipelines/explain_query.py:對同一個查詢跑 EXPLAIN ANALYZE。"""
import duckdb

from pipelines.common import DUCKDB_PATH

con = duckdb.connect(str(DUCKDB_PATH))

# DuckDB table:通常是簡單的全表掃描

plan_table = con.execute("""
    EXPLAIN ANALYZE
    SELECT COUNT(*) FROM staging.company_basic_clean
""").fetchall()
for row in plan_table:
    print(row[0])
# 輸出(節錄):

# EXPLAIN_ANALYZE

# ------------------------------

# ┌────────────────────────────────────────┐

# │ EXPLAIN                                │

# └────────────────────────────────────────┘

# ┌─────────────────────────────┐

# │         PROJECTION          │

# │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │

# │          COUNT(*)           │

# │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │

# │             0               │

# │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │

# │          ~715k Rows         │

# └─────────────────────────────┘


# Parquet:通常會走 parallel scan

plan_parquet = con.execute("""
    EXPLAIN ANALYZE
    SELECT COUNT(*) FROM read_parquet('data/company_basic/dt=2025-12-06/parquet/*.parquet',
                                       hive_partitioning=true)
""").fetchall()
for row in plan_parquet:
    print(row[0])
# 輸出(節錄):

# ┌─────────────────────────────┐

# │         PARQUET_SCAN        │

# └─────────────────────────────┘

EXPLAIN ANALYZE 會印出 DuckDB 內部的執行計畫與每個節點的時間。對 staging.company_basic_clean 表(已經在 DuckDB 檔案內),執行計畫通常很簡單:全表掃描加上平行化。對 Parquet 檔案,DuckDB 會用 PARQUET_SCAN 節點,這代表它只讀需要的 column chunk,不會把整個檔案載入記憶體。對 CSV 則會用 CSV_SCAN,並做 row group 預測,效能介於中間。

這個比較的關鍵洞察是「Parquet 不只省空間,更省時間」。即使我們用的是同一份資料邏輯(70 萬筆),Parquet 的讀取速度仍然是 CSV 的 3 倍左右。這對管線的整體成本有直接影響:每天多花 1 秒,累積一年就是 365 秒;以 24 小時全天跑的雲端機器計價,等於多花 0.1 小時 × $0.04 ≈ $0.004 美元。雖然數字很小,但對「一天跑 100 次」或「資料量放大 10 倍」的場景就值得重視。

完整實作:分區合併策略

當我們累積了 30 天、365 天、3 年的 Parquet 分區後,每次查詢都要 open 大量檔案。我們寫一支合併腳本:

"""de-journey/pipelines/compact_partitions.py:把多個 Parquet 分區合併成一個大檔。

策略:保留「最近 30 天」的小檔案(方便快速重跑),超過 30 天的合併成
單一檔案(減少檔案數)。
"""
import shutil
from datetime import date, timedelta
from pathlib import Path

import duckdb

from pipelines.common import DATA_DIR, DATASETS

con = duckdb.connect()
TODAY = date.today()
CUTOFF = TODAY - timedelta(days=30)


def compact(dataset_name: str, cfg: dict) -> None:
    """合併單一資料集的所有分區。"""
    base = DATA_DIR / cfg["partition_prefix"]
    if not base.exists():
        print(f"[{dataset_name}] 找不到 {base},跳過")
        return
    archive_dir = base / "archive"
    archive_dir.mkdir(exist_ok=True)
    # 把 30 天前的子資料夾合併到 archive/

    old_dirs = [
        d for d in base.glob("dt=*")
        if d.is_dir() and d.name[3:] < CUTOFF.isoformat()
    ]
    if not old_dirs:
        print(f"[{dataset_name}] 沒有 30 天以上的舊分區,跳過")
        return
    print(f"[{dataset_name}] 合併 {len(old_dirs)} 個舊分區")
    sources = [str(d / "parquet" / "*.parquet") for d in old_dirs]
    archive_path = archive_dir / f"merged-{TODAY:%Y%m%d}.parquet"
    con.execute(f"""
        COPY (
            SELECT * FROM read_parquet({sources},
                                       hive_partitioning=true)
        ) TO '{archive_path}' (FORMAT PARQUET, COMPRESSION zstd)
    """)
    # 合併完成後刪除原始的舊分區

    for d in old_dirs:
        shutil.rmtree(d)
    print(f"[{dataset_name}] 合併後檔案:{archive_path}({archive_path.stat().st_size:,} bytes)")


for name, cfg in DATASETS.items():
    compact(name, cfg)
con.close()

這支腳本的核心邏輯是「30 天為界」:30 天以內的分區保留小檔案(方便個別重跑),30 天以上的合併成單一檔案。合併後的檔案放在 archive/merged-YYYYMMDD.parquet,原始的小分區用 shutil.rmtree() 刪除。實際執行時間取決於資料量:以 30 天、每天 100 MB 的分區計算,合併約 5–10 秒。

合併策略有幾個變體可以考慮:第一種是「依月份合併」(每月 1 號合併上個月);第二種是「依檔案大小合併」(單一檔案超過 500 MB 才合併);第三種是「依查詢頻率合併」(常用的分區保留、冷資料合併)。我們這裡用「30 天」是最簡單的版本,對大多數管線已經足夠。

完整實作:成本估算腳本

把昨天的 COST_ESTIMATES 與實際的管線執行時間結合,估算每月雲端費用:

"""de-journey/pipelines/estimate_cost.py:估算管線每月的雲端成本。"""
import time
from pathlib import Path

from pipelines.common import COST_ESTIMATES, DATA_DIR, DUCKDB_PATH

# 收集實際檔案大小

total_csv_bytes = sum(p.stat().st_size for p in DATA_DIR.rglob("*.csv"))
total_parquet_bytes = sum(p.stat().st_size for p in DATA_DIR.rglob("*.parquet"))
warehouse_bytes = DUCKDB_PATH.stat().st_size if DUCKDB_PATH.exists() else 0

# 估算每月總 CPU 時間(每天一次)

seconds_per_run = sum(
    COST_ESTIMATES[k] for k in
    ["ingest_sec", "transform_sec", "build_marts_sec", "check_quality_sec"]
)
runs_per_month = 30  # 每天跑一次

total_seconds_per_month = seconds_per_run * runs_per_month
total_hours_per_month = total_seconds_per_month / 3600

# Lambda 風格計價(每 100ms 收費一次)

lambda_cost = total_seconds_per_month * COST_ESTIMATES["hourly_rate_usd"] / 3600

# EC2 風格計價(整天 24h 開機)

ec2_cost = 24 * 30 * COST_ESTIMATES["hourly_rate_usd"]

print("=== 儲存成本 ===")
print(f"CSV 總量:{total_csv_bytes/1e6:.1f} MB")
print(f"Parquet 總量:{total_parquet_bytes/1e6:.1f} MB")
print(f"DuckDB 檔案:{warehouse_bytes/1e6:.1f} MB")
print()
print("=== 運算成本(每月) ===")
print(f"每次管線 CPU 時間:{seconds_per_run} 秒")
print(f"每月總 CPU 時間:{total_seconds_per_month:.0f} 秒 ({total_hours_per_month:.2f} 小時)")
print(f"Lambda 計價(依執行時間):${lambda_cost:.4f} 美元")
print(f"EC2 計價(24h 開機):${ec2_cost:.2f} 美元")

這支腳本分兩部分:上半部統計儲存(CSV、Parquet、DuckDB 三者的磁碟用量),下半部估算運算成本。Lambda 計價的關鍵是「實際執行時間」——我們每天跑 115 秒、一個月 30 次,等於 0.96 小時,乘上 $0.04/小時得到 $0.038 美元;EC2 計價則是固定 24 × 30 = 720 小時 × $0.04 = $28.80 美元。

這個對比告訴我們「管線頻率低時 Lambda 划算、頻率高時 EC2 划算」。如果我們的管線每天只跑 1 次、每次 2 分鐘,Lambda 每月只要 $0.04;如果一天跑 100 次、每次 2 分鐘,Lambda 每月就要 $4 美元,開始接近一台小型 EC2。這是用 Lambda 跑管線的決策門檻。

"""de-journey/pipelines/profile_query.py:用 EXPLAIN PROFILE 找出瓶頸節點。"""
import duckdb

from pipelines.common import DUCKDB_PATH

con = duckdb.connect(str(DUCKDB_PATH))
# 對一個複雜查詢跑 EXPLAIN PROFILE,會列出每個節點的耗時

result = con.execute("""
    EXPLAIN PROFILE
    SELECT
        d.company_location,
        COUNT(*) AS n,
        AVG(f.change_date IS NOT NULL::int) AS change_rate
    FROM mart.dim_company d
    LEFT JOIN mart.fact_company_change f
        ON d.uniform_no = f.uniform_no
    WHERE d.capital_amount >= 1000000
    GROUP BY d.company_location
    ORDER BY n DESC
    LIMIT 20
""").fetchall()
for row in result:
    print(row[0])
# 輸出(節錄):

# ┌──────────────────────────┐

# │       HASH_JOIN          │

# │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │

# │  ~715k rows in           │

# │   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   │

# │  Time: 0.45s             │

# └──────────────────────────┘

EXPLAIN PROFILE 比 EXPLAIN ANALYZE 更詳細:除了總時間,還會列出每個節點(HASH_JOIN、PARQUET_SCAN、AGGREGATE)的耗時。當查詢效能出問題時,用 EXPLAIN PROFILE 找出最慢的節點,再針對那個節點做優化。

常見錯誤與踩雷

錯誤一:以為 Parquet 一定比 CSV 快 10 倍。常見症狀:期待 Parquet 帶來 10 倍效能提升,實際只看到 1.5–3 倍。對應排查方向:Parquet 的優勢主要在「只讀需要的欄位」與「壓縮後 I/O 變少」。如果查詢需要讀所有欄位、且資料量不大,差距就有限。我們的測試顯示對 70 萬筆的小資料集差距約 3 倍;對 7000 萬筆的大資料集差距會拉到 5–10 倍。

錯誤二:合併分區時忘記保留 dt= 欄位,導致資料時序資訊丟失。常見症狀:合併後的 Parquet 檔案沒有 dt 欄位,下游查詢「過去 90 天的變化」時沒辦法用 dt 篩選。對應排查方向:DuckDB 的 hive_partitioning=true 會自動把 dt=2025-12-06 這種路徑結構解析為 dt 欄位;合併時用 COPY (SELECT * FROM read_parquet(...)) 會把 dt 欄位一起帶進去。如果合併後的檔案要放在 archive/ 就不會有 dt 路徑,這時候要在 SQL 中明確 SELECT *, '2025-01-01' AS dt FROM ... 補上一個代表日期。

錯誤三:EXPLAIN ANALYZE 在跑多次的查詢上顯示的時間不準。對應排查方向:EXPLAIN ANALYZE 執行一次就退出,沒有「暖機」。第一次跑的時間通常比較長(包含 page cache 載入),建議先暖機一次再執行 EXPLAIN ANALYZE,或用我們上面的 benchmark_storage.py 跑 5 次取平均。

錯誤四:把成本估算的數字當成精確值。對應排查方向:雲端計價會隨促銷、預留實例、Spot 價格浮動。建議把估算結果當成「±30% 的區間」而非精確數字。例如本篇的 Lambda $0.04 美元,實際可能是 $0.03 到 $0.06 之間。這個區間對決策足夠,但對財務報表還是要算精確。

錯誤五:合併後忘記刪除原始小檔案,導致儲存成本翻倍。對應排查方向:我們的腳本用 shutil.rmtree() 自動刪除,但實務上建議「先移到 archive_pending/、確認合併檔案可用後再刪除」。這個兩階段合併能避免「合併失敗但原始檔已刪除」的災難。

效能與實務提醒

這是端到端管線專案的第五篇,也是收尾。讓我們把整個系列的成本與效能特性整理成一張表:

階段CPU 時間瓶頸優化手段
採集 (ingest)60 秒網路下載並行下載、增量載入
轉換 (transform)30 秒CSV 解析用 Parquet 取代 CSV
建模 (build_marts)15 秒DuckDB join預先 group_by、避免大表 join
品質 (check_quality)10 秒多條規則掃描合併規則為一個 UNION ALL
通知 (notify)2 秒HTML 渲染快取 HTML 樣板
展示 (Streamlit)隨查詢變動使用者互動@st.cache_data

從這張表可以看出,整條管線每次跑約 2 分鐘、每天跑一次、月跑 60 分鐘。這是非常「划算」的數字——大部分時間都花在「網路下載」與「CSV 解析」上,這兩個瓶頸都可以在 Day 36 / Day 37 部署到雲端時透過「網路速度提升」與「Parquet 取代 CSV」解決。

另一個重要的提醒:效能與成本是動態的。今天我們用 70 萬筆資料估算;如果明年公司數量翻倍到 140 萬筆,所有數字都會跟著翻。建議每季重新跑一次 benchmark_storage.py 與 estimate_cost.py,把數字存到 meta.benchmark_history 與 meta.cost_history 表,建立長期的趨勢監控。

小結

今天為端到端管線系列做了一個有具體數字的結尾,並把這些數字整理成可重現的腳本。我們用 EXPLAIN ANALYZE 比較了 CSV / Parquet / DuckDB table 的查詢時間差距;寫了 compact_partitions.py 處理分區合併;用 estimate_cost.py 估算每月雲端費用。重點回顧:第一,Parquet 比 CSV 快 3 倍以上(在我們的小資料集),差距會隨資料量放大;第二,分區合併是長期維運的必要動作,建議每季合併一次;第三,成本估算要區分 Lambda(依執行時間)與 EC2(24h 開機)兩種模式;第四,效能與成本是動態的,需要長期監控;第五,EXPLAIN ANALYZE 是 DuckDB 最實用的效能分析工具,比直接量測更精準。

明天 Day 36 會進入部署篇:把這條管線打包成 Docker 映像、用 docker-compose 啟動排程機器。Day 37 會用 GitHub Actions 做「零成本」的排程替代方案。整個專案系列(Day 30–Day 37)結束後,你會有一條可以部署到任何雲端的完整資料管線。

結語

今天的重點是「用數字說話」。效能與成本是很容易被忽略的章節,但對「決定用什麼等級的雲端機器」與「判斷管線是否健康」都是必備的。今天的 benchmark_storage.py 與 estimate_cost.py 應該被加進每月一次的維運清單——當資料量成長時,這兩支腳本能讓我們第一時間知道「需要升級機器了」還是「目前還撐得住」。

明天開始進入部署篇。我們會把這條管線打包成 Docker 映像,讓部署變成「拉映像、設定環境變數、啟動容器」三步;接著 Day 37 會用 GitHub Actions 提供「免費但有限制」的雲端排程替代方案。兩條路徑各有利弊,實務上可以同時採用:Docker 用在「有專屬機器」的場景、GitHub Actions 用在「輕量級的每日任務」。

延伸資源

  • DuckDB EXPLAIN 官方文件(2025,1.4 版):https://duckdb.org/docs/stable/guides/performance/explain。EXPLAIN ANALYZE 是 DuckDB 查詢效能分析的標準工具,本篇的計畫解讀以此文件為準。
  • Parquet 與 CSV 效能比較基準(2025):https://duckdb.org/docs/stable/guides/performance/file_formats。官方提供的基準測試涵蓋多種查詢類型,是評估儲存格式的重要參考。
  • AWS Lambda 定價(2025):https://aws.amazon.com/lambda/pricing/。本篇的 Lambda 計價以 2025 年 11 月的標準方案為基準。
  • AWS EC2 定價(2025):https://aws.amazon.com/ec2/pricing/。本篇的 EC2 計價以 2 vCPU 的 t3.small 為基準。
  • 12-Factor App 的「管理工作流程」原則:https://12factor.net/admin-processes。本篇的 compact_partitions.py 是典型的管理工作流程——獨立於管線主路徑、定期執行。
  • 政府資料開放平臺公司登記資料檢視頁:https://data.gov.tw/。本系列使用的「公司登記資料」與「公司變更登記資料」位於此平台,授權為「政府資料開放授權條款第 1 版」。
  • Day 30 採集與落地:parquet 落地格式的設計。本篇的效能與成本估算都建立在這個設計上。

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...