Learning Objectives
- 分辨長表與寬表,說出整齊資料(tidy data)的三條規則
- 用 pivot_table 把長表轉成寬表,說明 aggfunc 在什麼時候會用到、寬表的 NaN 代表什麼
- 用 melt 把寬表轉回長表,再接 groupby 或 pivot_table
- 用 crosstab 做兩個類別欄位的交叉表,並用 normalize、margins 算比例與總和
同一份資料,兩種形狀
同樣是三位學生、兩門課的期末成績,可以寫成兩種形狀:
長表(一列一筆修課):
| 學號 | 課程 | 期末 |
|---|---|---|
| S1 | 微積分 | 62 |
| S1 | 英文 | 80 |
| S2 | 微積分 | 71 |
| S2 | 英文 | 75 |
| S3 | 英文 | 90 |
寬表(一列一位學生、一欄一門課):
| 學號 | 微積分 | 英文 |
|---|---|---|
| S1 | 62 | 80 |
| S2 | 71 | 75 |
| S3 | NaN | 90 |
寬表適合給人看、做報表:一眼就能比較同一個人的各科成績。長表適合給程式算:
groupby("課程")、畫圖時依課程分色、篩選某一門課,都只要指定「課程」這一欄。
這一課學兩個方向的轉換:長轉寬用 pivot_table,寬轉長用 melt。
課堂資料 grades.csv、students.csv 是程式產生的模擬資料,不是任何學校的真實資料。
逐步看:長表轉寬表,再轉回來
先用 5 列的小長表。注意右邊的寬表:列是學號、欄是課程,S3 沒修微積分的那一格會是什麼?
整齊資料:一列一個觀察、一欄一個變數
Hadley Wickham 在 2014 年的論文〈Tidy Data〉(Journal of Statistical Software, 59(10))把「方便分析的形狀」整理成三條規則:
- 每一個變數是一欄。
- 每一個觀察是一列。
- 每一種觀察單位是一張表。
用這三條來看成績:「期中」和「期末」是同一個變數(分數)在兩次考試的值,還是兩個不同的變數? 要比較兩次考試時,把它們攤成「考試」與「分數」兩欄(長表)比較方便;要算每位學生的進步分數(期末 − 期中)時,留在同一列(寬表)比較方便。 所以沒有永遠正確的形狀,依接下來要做的計算選形狀,需要時再轉。第 3 條規則也解釋了上一課為什麼資料會分成學生表、成績表、社團表: 它們的觀察單位不同(一位學生、一筆修課、一筆社團紀錄)。
逐步看:600 筆成績轉成 200 × 6 的寬表
換成真正的課堂資料。每位學生只修其中幾門課,所以寬表會有很多 NaN。後半段先合併系所(上一課),再做「系所 × 課程」的平均表。
逐步看:把期中、期末攤成長表
成績表裡「期中」「期末」是兩欄。想用一次 groupby 比較兩次考試,就先 melt。
交叉表:兩個類別欄位各有幾筆
兩個欄位都是類別(例如系所與是否住宿)時,最常問的是「每一種組合各有幾個人」。pd.crosstab(列, 欄) 直接算出這張次數表,
等於 pivot_table(..., aggfunc="count") 的簡寫:
| 參數 | 作用 |
|---|---|
margins=True |
加上每列、每欄的總和(名稱是 All) |
normalize="index" |
每一列加起來是 1(各系內部的比例) |
normalize="columns" |
每一欄加起來是 1 |
normalize="all" |
全部格子加起來是 1 |
比較人數不同的組時要看比例:企管系 52 人、物理系 15 人,直接比「住宿人數」沒有意義,比「住宿比例」才有。 第 21 課會用卡方檢定問:這些比例的差異,大到不太像是抽樣的偶然嗎?
逐步看:各系住宿的人數與比例
同一張交叉表先看人數,再看每一列的比例,最後畫成堆疊長條圖。
實作:各課程的期中與期末平均
讀入 grades.csv,先用 melt 把 midterm、final 攤成 exam 與 score 兩欄(保留 student_id、course),
再用 pivot_table 做出「列是課程、欄是考試」的平均表,四捨五入到小數 1 位後印出。
小測驗:轉換形狀
長表有欄位 student_id、course、final,共有 200 位學生、6 門課。pivot_table(index="student_id", columns="course", values="final") 的形狀是?
下面程式會印出什麼?
import pandas as pd
df = pd.DataFrame({"id": ["a", "a", "b"], "k": ["x", "x", "y"], "v": [1, 3, 5]})
print(df.pivot_table(index="id", columns="k", values="v").loc["a", "x"])
下面程式會印出什麼?
import pandas as pd
w = pd.DataFrame({"id": [1, 2, 3], "q1": [5, 4, 3], "q2": [2, 2, 1], "q3": [4, 5, 5]})
print(w.melt(id_vars="id").shape)
小測驗:pivot 與交叉表
pd.crosstab(students["dept"], students["dorm"], normalize="index") 的結果有什麼特性?
pivot(不是 pivot_table)遇到同一個 id、同一個 k 有兩列時會怎樣?
import pandas as pd
df = pd.DataFrame({"id": ["a", "a"], "k": ["x", "x"], "v": [1, 3]})
print(df.pivot(index="id", columns="k", values="v"))
重點整理
- 長表一列一筆觀察,適合
groupby、篩選與畫圖;寬表一列一個對象,適合給人比較與做報表。 - 整齊資料(Wickham, 2014):每個變數一欄、每個觀察一列、每種觀察單位一張表。依接下來的計算選形狀。
pivot_table(index=, columns=, values=, aggfunc=)長轉寬;同一格有好幾筆時用 aggfunc 彙總(預設 mean)。寬表的 NaN 代表「沒有這個組合」。melt(id_vars=, value_vars=, var_name=, value_name=)寬轉長;寬表的每一格變成一列,缺值也算,需要時再dropna()。pd.crosstab算兩個類別欄位的次數表;margins=True加總和,normalize="index"看每一列的比例。- 下一課處理時間:把文字轉成日期、依週或月彙總、算移動平均,以及「一天」該從幾點算起。