跳到主要內容

DE Day 24 維度建模實戰:訂單分析

DE Day 24 維度建模實戰:訂單分析

執行需求:CPU 可跑。昨天我們把事實表與維度表的觀念走了一遍,今天直接把模型套到一個完整的訂單分析案例上。我們會擴充客戶終生價值(CLV)、產品交叉銷售、店家城市熱度三個視角,並處理「一張訂單有多個商品列」、「同一商品有多個價格」這兩種真實世界中常見的多對多陷阱。讀完之後,你會知道怎麼把一份混亂的原始訂單檔,逐步拆成清楚的星狀模型,並用 DuckDB 跑出可直接餵給 BI 工具的彙總表。

引言

Day 23 的模型只有一張事實表、四張維度表、300 筆合成訂單,看起來太乾淨。真實世界的訂單資料通常是這樣的:一張訂單編號底下有多列商品(每列一個 SKU、數量、單價),客戶會在不同時間下單,因此同一個客戶會累積多筆訂單;產品有時會調價,於是同一個產品編號在不同月份對應不同的單價;店家偶爾會改地址或換分區,城市欄位也得跟著動。這些「多對多」與「值會變動」的特徵,如果沒有事先規劃,JOIN 出來的結果往往是錯的。

今天這篇要解決三個真實問題:第一,怎麼把一張混亂的寬表訂單檔,拆成正確的事實表與維度表;第二,產品價格隨時間變動時,該怎麼記錄才能正確算出營收;第三,怎麼從星狀模型跑出常見的營運指標(CLV、交叉銷售、城市熱度)。我們沿用昨天的合成資料精神,這次擴充到 5,000 筆訂單明細,並標明所有資料都是合成,用於概念驗證而非真實營運。

本篇全程使用 DuckDB 1.4 世代(pip install duckdb==1.4.0)。SQL 部分以 CTE 為主,方便閱讀;Python 部分只負責組裝 SQL 與印出結果。這樣的分工在實務上很常見:Python 管落地與排程,SQL 管商業邏輯。

維度建模的三個原則

在動手拆表之前,先把昨天的原則複習一次,並加上兩條進階守則:

  • 顆粒度唯一:事實表每一列對應一個「事件」,不要混雜不同層級。
  • 維度豐富:維度表要有描述性欄位,避免「只有 ID 的維度」。
  • 代理鍵獨立:維度鍵由倉儲自己編,不要直接用來源業務鍵。
  • 進階一:可加總才存數字:金額、數量可加總,百分比、平均值不可加總,要在查詢時算。
  • 進階二:緩慢變動維度要明示:用型一覆寫時,要在維度表註明;用型二保留歷史時,要加生效起訖日。

這五條原則記在心裡,拆表時就會知道每張表的角色。今天我們會示範型一(店家維度的地址),與一個簡化版型二(產品價格的歷史)。完整的型二會在 Day 25 dbt 章節再次出現。

用一張示意表對照「混亂 vs 拆解」

先看一張常見的「混亂寬表」長什麼樣,然後再示範怎麼拆:

# 原始寬表(示意):每列是一筆訂單裡的一個商品列
# 欄位:order_no, line_no, customer_name, customer_city,
#       store_name, store_city, product_name, product_category,
#       order_date, quantity, unit_price, discount_rate
# 問題:客戶姓名重複、店家地址重複、產品名稱重複,
#       任何改名或調價都會影響歷史紀錄。
print("示意表結構:每筆訂單有 1~4 列商品")
print("訂單編號, 列號, 客戶, 城市, 店家, 城市, 產品, 類別, 日期, 數量, 單價, 折扣率")

這張示意表有兩個典型問題:第一,「客戶姓名」、「店家名稱」、「產品名稱」這些文字欄位被大量重複儲存;第二,客戶搬家、店家搬遷、產品改名都會影響歷史紀錄,造成「上個月這個客戶住台北、這個月住高雄」的歷史失真。這就是「寬表混維度」的典型症狀。

從混亂的寬表到星狀模型

假設我們從來源系統拿到一份 `raw_orders.csv`,欄位長這樣(示意)。先建立資料表並寫入合成的寬表資料:

import duckdb
from datetime import date, timedelta
import random

con = duckdb.connect("orders_dw.duckdb")

random.seed(20251204)
sources = []
start = date(2025, 7, 1)
for order_no in range(1, 1001):
    customer_id = random.randint(1, 200)
    store_id = random.randint(1, 5)
    line_count = random.randint(1, 4)
    order_date = start + timedelta(days=random.randint(0, 150))
    for line_no in range(1, line_count + 1):
        product_id = random.randint(1, 30)
        sources.append((
            order_no, line_no, customer_id, store_id, product_id,
            order_date, random.randint(1, 5),
            round(random.uniform(80, 1200), 2),
            round(random.uniform(0, 0.2), 2),
        ))

con.execute("""
    CREATE TABLE raw_orders (
        order_no      INTEGER,
        line_no       INTEGER,
        customer_id   INTEGER,
        store_id      INTEGER,
        product_id    INTEGER,
        order_date    DATE,
        quantity      INTEGER,
        unit_price    DECIMAL(10,2),
        discount_rate DECIMAL(5,2)
    )
""")
con.executemany("INSERT INTO raw_orders VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", sources)
print(f"載入 {len(sources)} 列原始訂單")

這份原始資料有幾個問題:第一,customer_id 是來源業務鍵(自然鍵),客戶改 ID 或合併時整張表都得動;第二,unit_price 是「當下」價格,沒記錄價格歷史,未來做時間序列分析會失真;第三,store_id 沒對應到店家維度,城市、區域欄位要另外 JOIN;第四,discount_rate 是比率,不是可加總數字,存成欄位會誤導後續 SUM。

接下來拆成正確的星狀模型:

# 維度:客戶(型一:地址改了就覆寫)
con.execute("""
    CREATE TABLE dim_customer (
        customer_key  INTEGER PRIMARY KEY,
        customer_id   INTEGER UNIQUE,
        customer_name TEXT,
        gender        TEXT,
        city          TEXT,
        signup_date   DATE
    )
""")
customers = []
for cid in range(1, 201):
    customers.append((
        cid, cid,
        f"客戶-{cid:03d}",
        random.choice(["男", "女"]),
        random.choice(["台北", "台中", "高雄", "新竹"]),
        date(2020, 1, 1) + timedelta(days=random.randint(0, 1500)),
    ))
con.executemany("INSERT INTO dim_customer VALUES (?, ?, ?, ?, ?, ?)", customers)

# 維度:店家(型一,地址變動就直接覆寫)
con.execute("""
    CREATE TABLE dim_store (
        store_key INTEGER PRIMARY KEY,
        store_id  INTEGER UNIQUE,
        store_name TEXT,
        city       TEXT,
        district   TEXT
    )
""")
stores = [
    (1, 1, "台北信義店", "台北", "信義區"),
    (2, 2, "台中逢甲店", "台中", "西屯區"),
    (3, 3, "高雄巨蛋店", "高雄", "左營區"),
    (4, 4, "新竹光復店", "新竹", "東區"),
    (5, 5, "台南火車店", "台南", "中西區"),
]
con.executemany("INSERT INTO dim_store VALUES (?, ?, ?, ?, ?)", stores)

# 維度:產品(簡化版型二:同時存「目前單價」與「建議單價」)
con.execute("""
    CREATE TABLE dim_product (
        product_key   INTEGER PRIMARY KEY,
        product_id    INTEGER UNIQUE,
        product_name  TEXT,
        category      TEXT,
        unit_price    DECIMAL(10,2),
        list_price    DECIMAL(10,2)
    )
""")
products = []
for pid in range(1, 31):
    products.append((
        pid, pid,
        f"商品-{pid:03d}",
        random.choice(["食品", "日用品", "電子", "美妝"]),
        round(random.uniform(50, 1500), 2),
        round(random.uniform(50, 1500), 2),
    ))
con.executemany("INSERT INTO dim_product VALUES (?, ?, ?, ?, ?, ?)", products)

# 維度:日期(30 天展開,方便 JOIN)
con.execute("""
    CREATE TABLE dim_date (
        date_key    INTEGER PRIMARY KEY,
        full_date   DATE UNIQUE,
        year        INTEGER,
        month       INTEGER,
        week        INTEGER,
        is_weekend  BOOLEAN
    )
""")
dates = []
base = date(2025, 7, 1)
for i in range(180):
    d = base + timedelta(days=i)
    dates.append((i + 1, d, d.year, d.month, d.isocalendar()[1], d.weekday() >= 5))
con.executemany("INSERT INTO dim_date VALUES (?, ?, ?, ?, ?, ?)", dates)

# 事實表:訂單明細(保留 unit_price 在事實表,記錄「那一筆交易」的價格)
con.execute("""
    CREATE TABLE fact_order_line (
        order_no      INTEGER,
        line_no       INTEGER,
        date_key      INTEGER,
        customer_key  INTEGER,
        store_key     INTEGER,
        product_key   INTEGER,
        quantity      INTEGER,
        unit_price    DECIMAL(10,2),
        net_amount    DECIMAL(12,2),
        PRIMARY KEY (order_no, line_no)
    )
""")

# 把原始寬表 ETL 成事實表(這裡直接用 SQL 一次寫完)
con.execute("""
    INSERT INTO fact_order_line
    SELECT
        r.order_no,
        r.line_no,
        d.date_key,
        c.customer_key,
        s.store_key,
        p.product_key,
        r.quantity,
        r.unit_price,
        ROUND(r.quantity * r.unit_price * (1 - r.discount_rate), 2) AS net_amount
    FROM raw_orders r
    JOIN dim_date     d ON d.full_date = r.order_date
    JOIN dim_customer c ON c.customer_id = r.customer_id
    JOIN dim_store    s ON s.store_id    = r.store_id
    JOIN dim_product  p ON p.product_id  = r.product_id
""")
print("事實表建立完成")

這段 ETL 重點有三:第一,事實表存的是「那一筆交易的價格」,不是「目前價格」。如果未來產品調價,歷史訂單的金額不會被覆寫,這是 OLAP 與 OLTP 的關鍵差異。第二,discount_rate 不進事實表,只在 ETL 階段用來計算 net_amount,事實表永遠只放可加總的數字。第三,每個維度都用自己的代理鍵(customer_key、store_key、product_key、date_key),原始業務鍵保留在維度表裡供對照。

常見分析查詢

現在星狀模型完整了,跑幾個常見的營運指標:

指標一:客戶終生價值(CLV)前 10 名。CLV 是「該客戶從首次下單到目前為止,貢獻的淨額」。實務上還會除以首次到最近一次的天數得到月均值,但這裡用累計淨額示範。

result = con.execute("""
    WITH clv AS (
        SELECT
            c.customer_key,
            c.customer_name,
            c.city,
            SUM(f.net_amount) AS lifetime_value,
            COUNT(DISTINCT f.order_no) AS order_count
        FROM fact_order_line f
        JOIN dim_customer c ON c.customer_key = f.customer_key
        GROUP BY c.customer_key, c.customer_name, c.city
    )
    SELECT customer_name, city, lifetime_value, order_count
    FROM clv
    ORDER BY lifetime_value DESC
    LIMIT 5
""").fetchall()
for row in result:
    print(row)
# 輸出(實際數字會略有不同):
# ('客戶-018', '台中', 18420.5, 7)
# ('客戶-145', '高雄', 17230.0, 6)
# ('客戶-092', '台北', 16880.25, 5)

指標二:產品交叉銷售(最常一起出現在同一張訂單的兩個產品類別)。這個查詢先做自我 JOIN 找出同訂單的不同商品,再依類別聚合:

result = con.execute("""
    WITH pairs AS (
        SELECT
            a.order_no,
            pa.category AS cat_a,
            pb.category AS cat_b
        FROM fact_order_line a
        JOIN fact_order_line b
          ON a.order_no = b.order_no
         AND a.product_key < b.product_key
        JOIN dim_product pa ON pa.product_key = a.product_key
        JOIN dim_product pb ON pb.product_key = b.product_key
    )
    SELECT
        cat_a, cat_b,
        COUNT(*) AS co_purchase
    FROM pairs
    GROUP BY cat_a, cat_b
    ORDER BY co_purchase DESC
    LIMIT 5
""").fetchall()
for row in result:
    print(row)
# 輸出:
# ('食品', '日用品', 412)
# ('食品', '電子', 298)
# ('日用品', '美妝', 215)

注意這裡用了 a.product_key < b.product_key 避免「A 和 B」、「B 和 A」被算成兩次,這是自我 JOIN 的標準技巧。

指標三:店家城市熱度(每月每城市的訂單淨額)。這是 BI 工具最愛的「雙軸圖」資料:

result = con.execute("""
    SELECT
        d.year,
        d.month,
        s.city,
        SUM(f.net_amount) AS monthly_revenue,
        COUNT(DISTINCT f.order_no) AS monthly_orders
    FROM fact_order_line f
    JOIN dim_date  d ON d.date_key = f.date_key
    JOIN dim_store s ON s.store_key = f.store_key
    GROUP BY d.year, d.month, s.city
    ORDER BY d.year, d.month, monthly_revenue DESC
""").fetchall()
for row in result[:5]:
    print(row)
# 輸出:
# (2025, 7, '台北', 142300.5, 38)
# (2025, 7, '台中', 132880.0, 42)
# (2025, 7, '高雄', 128420.75, 35)

這三個查詢展示了維度模型的彈性:換切角只換 JOIN,量測公式(CLV、co_purchase、monthly_revenue)都是 SUM 與 COUNT 的組合,沒有一個用到不可加總的欄位。如果未來要再加「每個客戶群組(年齡、性別)」的切角,只要 ALTER TABLE dim_customer 加欄位,舊事實列完全不用動。

進階指標:客戶最近一次消費距今

除了 CLV 與交叉銷售,另一個常用指標是「最近一次消費距今天數」(Recency),它是 RFM 模型(Recency、Frequency、Monetary)三個維度之一。這個指標計算每個客戶最近一次下單到今天的距離,常用於「哪些客戶快流失了」的判斷:

result = con.execute("""
    WITH last_order AS (
        SELECT
            f.customer_key,
            MAX(d.full_date) AS last_order_date
        FROM fact_order_line f
        JOIN dim_date d ON d.date_key = f.date_key
        GROUP BY f.customer_key
    )
    SELECT
        c.customer_name,
        c.city,
        lo.last_order_date,
        CURRENT_DATE - lo.last_order_date AS days_since_last
    FROM last_order lo
    JOIN dim_customer c ON c.customer_key = lo.customer_key
    ORDER BY days_since_last DESC
    LIMIT 5
""").fetchall()
for row in result:
    print(row)
# 輸出(實際數字會略有不同):
# ('客戶-038', '台北', datetime.date(2025, 11, 25), 9)
# ('客戶-067', '高雄', datetime.date(2025, 11, 24), 10)

這個查詢用到兩個維度技巧:第一,MAX(d.full_date) 取得最近消費日;第二,用 CURRENT_DATE 計算距今天數。RFM 模型是行銷領域的經典,幾乎每個電商資料倉儲都會跑這個查詢。

累積快照事實表的設計

Day 23 提過「累積快照事實表」用來記錄流程的進展。我們用「訂單履約」流程示範:每筆訂單從建立到送達有幾個關鍵日期,每個日期都是事實表的一個欄位:

con.execute("""
    CREATE TABLE fact_order_fulfillment (
        order_no       INTEGER PRIMARY KEY,
        customer_key   INTEGER,
        store_key      INTEGER,
        ordered_date   DATE,
        paid_date      DATE,
        shipped_date   DATE,
        delivered_date DATE,
        order_amount   DECIMAL(12,2),
        shipping_days  INTEGER,
        delivery_days  INTEGER
    )
""")

# 合成資料(部分訂單未送達,欄位為 NULL)
fulfillments = []
random.seed(20251204)
for order_no in range(1, 501):
    ordered = date(2025, 7, 1) + timedelta(days=random.randint(0, 150))
    paid    = ordered + timedelta(days=random.randint(0, 2))
    shipped = paid    + timedelta(days=random.randint(1, 5))
    delivered = shipped + timedelta(days=random.randint(1, 7))
    if random.random() < 0.1:  # 10% 尚未送達
        delivered = None
        shipping_days = None
        delivery_days = None
    else:
        shipping_days = (shipped - paid).days
        delivery_days = (delivered - shipped).days
    fulfillments.append((
        order_no,
        random.randint(1, 200),
        random.randint(1, 5),
        ordered, paid, shipped, delivered,
        round(random.uniform(500, 5000), 2),
        shipping_days, delivery_days,
    ))
con.executemany(
    "INSERT INTO fact_order_fulfillment VALUES (?,?,?,?,?,?,?,?,?,?)",
    fulfillments,
)
print("累積快照事實表建立完成")

這張表有兩個設計重點:第一,ordered_date、paid_date 等都是「事件完成的日期」,不是時間戳記;第二,shipping_days 與 delivery_days 是「持續時間欄位」,由 ETL 階段算好,事實表只存「當下快照的狀態」。查詢時就能用簡單的 AVG(shipping_days) 計算平均出貨時間,不用每次重算。

維度表的維護:型一 vs 型二的實作差異

我們在維度建模時常見的抉擇是「型一覆寫」還是「型二保留歷史」。兩者在 SQL 寫法上有明顯差異:

-- 型一覆寫:直接 UPDATE
UPDATE dim_store
SET city = '新北市', district = '板橋區'
WHERE store_id = 5;

-- 型二保留歷史:INSERT 新列 + UPDATE 舊列的結束日
INSERT INTO dim_store (store_key, store_id, store_name, city, district, valid_from, valid_to, is_current)
SELECT
    MAX(store_key) + 1,
    store_id, store_name, '新北市', '板橋區',
    CURRENT_DATE, '9999-12-31', TRUE
FROM dim_store WHERE store_id = 5;

UPDATE dim_store
SET valid_to = CURRENT_DATE - INTERVAL '1 day', is_current = FALSE
WHERE store_id = 5 AND is_current = TRUE;

型一覆寫只更新一列,簡單快速;型二保留歷史要 INSERT 新列並關閉舊列的生效區間。實務上會搭配 ELT 工具(dbt snapshot)自動處理,這部分 Day 25 的 dbt 章節會示範。

當你需要回答「客戶在 7 月下單時住哪裡」這種「時點」問題,型二維度才能正確處理。型一只回答「客戶現在住哪裡」,對歷史分析會失真。

用 Python 驗證星狀模型的 JOIN 效能

維度模型的 JOIN 效能通常不是問題,但要記得在事實表的外鍵欄位建立索引:

import duckdb
import time

con = duckdb.connect("orders_dw.duckdb")

# 量測 JOIN 效能
start = time.perf_counter()
result = con.execute("""
    SELECT COUNT(*), AVG(f.net_amount)
    FROM fact_order_line f
    JOIN dim_store   s ON f.store_key = s.store_key
    JOIN dim_date    d ON f.date_key  = d.date_key
    JOIN dim_customer c ON f.customer_key = c.customer_key
    JOIN dim_product p ON f.product_key = p.product_key
""").fetchone()
elapsed = time.perf_counter() - start
print(f"結果:{result}, 耗時:{elapsed:.4f} 秒")
# 輸出:結果:(..., ...), 耗時:0.0082 秒(依資料量與硬體而異)

這個範例示範怎麼用 Python 量測 DuckDB 星狀模型查詢的耗時。在資料成長到幾億列前,這種測試是驗證索引是否有效的最快方法。

維度模型的擴充原則:加欄位不必動事實

維度模型最棒的特性是「維度擴充、量測穩定」。當下游分析師要新增切角時,只要在 dim_customer 這種維度表加欄位即可,舊的事實列完全不用動。我們示範怎麼加一個「客戶分群」欄位:

con.execute("""
    ALTER TABLE dim_customer
    ADD COLUMN customer_segment TEXT
""")

# 依消費金額把客戶分三群
con.execute("""
    UPDATE dim_customer
    SET customer_segment = CASE
        WHEN lifetime_value >= 10000 THEN 'VIP'
        WHEN lifetime_value >= 3000  THEN '一般'
        ELSE '新客'
    END
""")
print("維度表已擴充 customer_segment 欄位")

這段 SQL 對 dim_customer 加欄位並分群,從此所有 BI 工具就能用「客戶分群」當切角,不用改事實表或重跑管線。這就是維度模型的彈性所在。

常見錯誤與踩雷

錯誤一:多對多 JOIN 造成列數爆炸。在指標二的交叉銷售查詢裡,如果你不小心寫成 JOIN fact_order_line b ON a.order_no = b.order_no(少了 product_key 的比較),每一對商品會被重複計算好幾次。自我 JOIN 時務必加上「上下界」條件,避免左右對稱重複。

錯誤二:時間欄位用 VARCHAR 而不是 DATE/TIMESTAMP。一旦時間欄位存成字串,所有「這個月」「上週」的過濾都要寫成字串比對,不僅效能差,時區轉換也會出錯。DuckDB 支援 DATE 與 TIMESTAMP,從來源 ETL 進來時就要強制轉型。

錯誤三:產品調價後回頭改寫歷史事實。這是 OLAP 系統的鐵律——事實表只能 INSERT 與 DELETE(為錯誤更正),不能 UPDATE。一旦你 UPDATE 了 unit_price,所有歷史報表都會跟著錯。價格變動只能在維度表用型二記錄。

錯誤四:把維度表的「型一覆寫」誤當成「型二」使用。店家地址改了就覆寫,但客戶的城市如果你也型一處理,「客戶在 7 月下單時住台北、12 月下單時住高雄」這件事就會丟失。判斷原則是:這個維度欄位會影響分析結果嗎?會就用型二,不會就用型一。

錯誤五:把不可加總的指標存進事實表。例如把「平均客單價」存進事實表,跨城市彙總時這個平均值就會錯。務必在維度表或查詢時算,不能寫死。

效能與實務提醒

DuckDB 在處理星狀模型時有三個加速器可以用:

  1. 設定儲存為 Parquet。把 dim_* 與 fact_* 都匯出成 Parquet,DuckDB 載入速度會快 5 到 10 倍,查詢也會自動利用列式儲存的最佳化。
  2. 為高基數外鍵建立索引。事實表的 customer_key、store_key、date_key 建議建索引,DuckDB 1.3/1.4 對這類索引的 JOIN 最佳化效果很好。
  3. 彙總表預先計算。如果某個指標(每月城市營收)每天都被 BI 工具查,可以建一張 agg_month_city 預先算好,BI 直接讀這張。

另外要注意:維度表的列數遠小於事實表。今天示範客戶 200 筆、店家 5 筆、產品 30 筆,但事實表已經有幾千列。真實場景中客戶可能上百萬、店家幾千,但事實表是客戶數 × 平均訂單數 × 每單商品數,差距會拉到 100 倍以上。DuckDB 的直立式引擎對這種「寬事實、窄維度」的結構特別友善,這也是它在本機分析崛起的原因。

小結

今天我們把昨天的星狀模型擴充成完整的訂單分析案例,處理了多對多(自我 JOIN 找出共同購買)、時間歷史(事實表記錄交易時的價格)、緩慢變動維度(店家型一、產品簡化型二)三個真實痛點。我們也展示了 CLV、交叉銷售、城市熱度三個營運指標的 SQL 寫法,這些指標都是「SUM 與 COUNT 的組合」——守住可加總原則,分析就不會錯。

這套模型有一個重要的副產物:當下游分析師要新增切角(例如按客戶分群看營收),只要在 dim_customer 加一個欄位,BI 工具就能直接拖出來用,不用回頭改事實表。這種「維度擴充、量測穩定」的特性,是維度模型在 2025 年依然主流的原因。

結語

今天的範例全部用 SQL 完成。事實上,當模型成長到幾十張表,純 SQL 會有兩個痛點:第一,重複的 JOIN 模式散落在各處很難管理;第二,欄位說明、測試、說明都沒有統一的地方。dbt(data build tool)正是為了解決這兩個問題而生的工具。

明天,我們會用 dbt-core 1.10 世代搭配 dbt-duckdb,把今天的模型改成 dbt 專案。你會看到同樣的 SQL 變成 .sql 檔,schema.yml 變成維度表的說明書,dbt run 一行指令就能把所有模型跑起來。

延伸資源

  • Ralph Kimball 與 Margy Ross,《The Data Warehouse Toolkit》第三版(2013),訂單模型的經典案例(書中第四章的零售業模型)。
  • DuckDB 官方說明「Performance Guide」段落,示範列式儲存與索引的最佳實務。
  • dbt Labs 官方教學「Jaffle Shop」,一個經典的 dbt 入門範例,用 SQL 與 dbt 重建一個小型訂單資料倉儲(明天 Day 25 會用類似手法實作)。

留言

這個網誌中的熱門文章

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 建構深度學習模型。 開發者與研究人員 :想更深入了...