M11 表格資料
- 把 list of dict 變成 DataFrame,分清 DataFrame 和 Series;
- 用
json_normalize把巢狀的訂單攤平成「一行一本書」的表格; - 用條件篩選、新增欄、
merge合併、groupby分組統計; - 畫出統計圖(中文正常顯示),把結果寫出 CSV 和 JSON。
1為甚麼要表格 Why tables
M5 計算「每本書賣出多少本」,寫了兩層迴圈和一個計數的 dict。現在店長又問:「每個月賣了多少錢?哪間分店賣得最好?八月和九月比較呢?」每個問題都要再寫一段迴圈。
這些問題有一個共通點:資料是一行一筆、欄位固定的表格,問題都是「按某一欄分組,計算另一欄」。pandas 是專門處理這種資料的套件。在專案裏加入它和畫圖用的 matplotlib:
uv add pandas matplotlib
本課用的是 pandas 3。網上不少教學是 pandas 1、2 時代寫的,個別寫法已經不同,下文遇到時會指出。
2DataFrame 和 Series DataFrames and Series
一個 list of dict(M4 最常見的資料形狀),直接就能變成一張表:
import pandas as pd
books = [
{"title": "看得見的資料", "price": 120, "stock": 3},
{"title": "給初學者的 Python", "price": 180, "stock": 0},
{"title": "設定檔的世界", "price": 95, "stock": 5},
]
df = pd.DataFrame(books)
title price stock
0 看得見的資料 120 3
1 給初學者的 Python 180 0
2 設定檔的世界 95 5
- DataFrame(資料表)是整張表;Series(序列)是其中一欄(或一行);
df["price"]取出一欄(Series);df[["title", "price"]]取出幾欄(仍是 DataFrame,留意兩層方括號);df.shape是(行數, 欄數);df.dtypes是每欄的型別:數字是int64、float64,文字在 pandas 3 是str。
對一欄做運算,會對每一行做,不用寫迴圈:
df["price"] * df["stock"] # 每本書的庫存值:360、0、475
(df["price"] * df["stock"]).sum() # 835
3讀入 CSV 和 JSON Reading files
df = pd.read_csv("books.csv", encoding="utf-8-sig")
read_csv 和 M10 的 csv 模組不同:它會猜每一欄的型別,"120" 讀入就是數字,方便得多。但 M9 的教訓同樣適用,猜就會猜錯:
pd.read_csv("contacts.csv") # phone 欄:0123456 → 123456,前面的 0 不見了
pd.read_csv("contacts.csv", dtype={"phone": str}) # 指明這欄是文字:'0123456'
編號、電話、代碼這類欄,用 dtype= 指明是字串。
4攤平巢狀的 JSON Flattening nested JSON
小書店的訂單是巢狀的:一張訂單有幾本書(M5)。表格只有行和列,要先攤平:一本書一行,訂單的資料在每一行重複。json_normalize 就是做這件事的:
orders = json.loads(Path("m11-orders.json").read_text(encoding="utf-8"))
items = pd.json_normalize(orders, record_path="items", meta=["id", "date", "customer", "branch"])
title qty id date customer branch
0 設定檔的世界 1 O001 2026-08-03 黃同學 機場分店
1 給初學者的 Python 2 O001 2026-08-03 黃同學 機場分店
2 給初學者的 Python 1 O002 2026-08-04 李先生 門市
訂單 O001 有兩本書,攤平後變成兩行,id、date、customer、branch 在兩行重複。這時還沒有價錢欄,下一節再合併。
巢狀的 dict(不是 list)更簡單,json_normalize 會把每一層用點號接起來:{"author": {"name": …}} 變成一欄 author.name。
5篩選、新增欄、合併 Filtering, new columns, merging
篩選:在方括號裏放一個條件,只留下條件成立的行:
df[df["stock"] == 0] # 缺貨的書
df[(df["stock"] > 0) & (df["price"] < 150)] # 有貨而且低於 150 元
pandas 的條件組合要用 &(而且)、|(或者),不能用 M5 的 and、or,否則會報 ValueError: The truth value of a Series is ambiguous。每個條件都要加括號:& 比 > 先算,漏了括號同樣會報這個錯。
新增欄:直接賦值,對每一行計算:
items["amount"] = items["price"] * items["qty"]
但訂單裏只有書名,沒有價錢。價錢在目錄裏。合併(merge)就是按某一欄把兩張表對上,和 M6 的 titles dict 查價錢是同一個意思:
catalog = pd.DataFrame(yaml.safe_load(Path("catalog.yaml").read_text(encoding="utf-8")))
items = items.merge(catalog[["title", "price"]], on="title", how="left")
how="left" 表示保留左邊(訂單)的每一行;目錄裏找不到的書,價錢是空值 NaN,不會靜靜地消失。
想把缺貨的書庫存改成 10,直覺會寫 df[df["stock"] == 0]["stock"] = 10。在 pandas 3 裏這一句甚麼都不會改,只會出一個 ChainedAssignmentError 警告。改資料一律用 .loc[行的條件, 欄名]:
df.loc[df["stock"] == 0, "stock"] = 10
6分組統計 Grouping
「每本書賣出多少本」、「每月賣了多少錢」都是同一個模式:按一欄分組,對另一欄計算。
flowchart LR
T["items 表格<br/>每本書一行"] -->|"分組<br/>groupby('title')"| G1["看得見的資料:2、1、3…"]
T --> G2["設定檔的世界:1、3…"]
T --> G3["…"]
G1 -->|"計算<br/>sum()"| R["結果<br/>每本書一行"]
G2 --> R
G3 --> R
classDef t fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
classDef g fill:#ffffff,stroke:#d3d1cb,color:#37352f
class T,R t
class G1,G2,G3 g
items.groupby("title")["qty"].sum().sort_values(ascending=False) # 每本書的銷量,由多到少
按月統計,先把日期字串轉成日期,再取出月份:
items["date"] = pd.to_datetime(items["date"])
items["month"] = items["date"].dt.to_period("M") # 2026-08、2026-09…
items.groupby("month")["amount"].sum() # 每月營業額
items.groupby(["month", "branch"])["amount"].sum() # 每月每間分店
一次計算幾樣東西,用 agg:
items.groupby("month").agg(訂單數=("id", "nunique"), 銷量=("qty", "sum"), 營業額=("amount", "sum"))
nunique 數有幾個不同的值:一張訂單攤平後有幾行,訂單編號重複出現,所以數訂單要用它,不能用行數。
7畫圖和寫出 Plotting and saving
統計的結果可以直接畫圖:
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["Heiti TC", "Microsoft JhengHei", "Noto Sans CJK TC", "Arial Unicode MS", "DejaVu Sans"]
monthly = items.groupby("month")["amount"].sum()
monthly.plot(kind="bar", title="每月營業額")
plt.show()
matplotlib 預設的字型沒有中文字,標題和標籤會變成一個個方格,並出現 Glyph … missing from font(s) DejaVu Sans 的警告。在畫圖之前加上上面那一行 font.sans-serif:Mac 會用到 Heiti TC,Windows 會用到 Microsoft JhengHei(微軟正黑體),清單裏第一個找得到的就會被用上。Linux 先安裝 fonts-noto-cjk,再刪除 ~/.cache/matplotlib 資料夾(matplotlib 的字型快取),重新啟動 kernel。JupyterLite 在瀏覽器裏執行,沒有中文字型,中文仍會顯示成方格。
寫出結果:
monthly.to_csv("monthly.csv", encoding="utf-8-sig") # 給 Excel 開(M10 的 BOM)
items.to_json("items.json", orient="records", force_ascii=False, indent=2)
orient="records" 寫出 list of dict(和讀入時的形狀一樣);force_ascii=False 就是 M8 的 ensure_ascii=False。
8容易混淆 Easily confused
| 這兩樣 | 分別 |
|---|---|
| DataFrame 和 Series | 前者是整張表;後者是一欄。 |
df["title"] 和 df[["title"]] |
前者得到 Series;後者得到只有一欄的 DataFrame。 |
& 和 and |
pandas 的條件組合用 &、` |
df[條件]["欄"] = 值 和 df.loc[條件, "欄"] = 值 |
前者在 pandas 3 不會改到資料;後者才會。 |
行數(len、count)和 nunique |
前者連重複的訂單編號也算(count 數的是非空值);後者數有幾個不同的值。數訂單要用後者。 |
read_csv 和 csv 模組 |
前者猜型別(方便,但會吃掉前面的 0);後者一律字串。 |
9動手:三個月的訂單 Hands-on: three months of orders
- 下載
m11-orders.ipynb和m11-orders.json,連同 M4 的catalog.yaml,放進python-course資料夾; - 在終端機執行
uv add pandas matplotlib; - 執行
uv run jupyter lab,打開 notebook,逐格執行。
小書店八至十月共 80 張訂單。你要把它們攤平、和目錄合併算出金額,再回答店長的問題:每月營業額、最暢銷的書、哪間分店賣得最好;畫兩張圖,最後寫出一份給會計部的 CSV。
m11-sensor-series-eng.ipynb 和 m11-sensor-hourly.csv:三日的每小時溫度讀數,中間斷線了三小時,還有一個明顯的異常值。把時間設為索引,用 resample 改為每六小時平均、找出缺了的時段、用滾動平均平滑雜訊,畫折線圖。
M12 看看身邊的設定檔:VS Code、GitHub Actions、docker-compose、Markdown 的 frontmatter,以及 Claude Code 的設定,全部都是 JSON 或 YAML。