即使內容相同,資料可以用許多不同的形式表示,其中,長與寬資料的資料形式差異,會改變資料科學家操作資料的方便程度,長寬資料的不同尤其對製作數據圖表有非常大的影響。
這則筆記將跟你分享區分長與寬資料為何能讓你的資料科學工作更有效率,並且教學 Python 的 Pandas 套件裡面好用的長寬資料轉換函式,讓你流暢地重塑資料。
目錄
長或寬資料,有什麼差別?
請讀者先看一眼以下圖示:
寬資料將同一個屬性的資料分成多個欄位,例如在上圖中,考試成績依照不同考試年份這個資料屬性,被分成了多個欄位。寬資料是對人類而言較為直覺好懂的資料表達方式,觀看表格時理解數據較不費力,尤其是需要編輯表格時,填表較為方便。
聽起來寬資料的優點很棒,為何還需要討論長資料呢?因為不同的統計分析工具會要求不同的資料格式,尤其是現在熱門的繪圖工具,包括 R 語言的 ggplot2、pandas 的 matplotlib、Tableau、或者 Power BI 等等,用長資料更能發揮他們的多樣繪圖功能。能夠流暢地在長寬資料之間切換才會讓資料科學家的分析更有效率。
長資料中,一個屬性只出現在唯一欄位,上圖中的長資料,考試年份的資訊被記錄在同一個欄位。長資料對電腦而言更好理解、也更好處理的格式,例如,假如我們現在要增加 John 在 2021 年新的考試成績、但是其他人沒有 2021 年的成績資料,在長資料格式只要簡單地增加一列即可;相對地,如果是寬資料格式,除了需要多插入一個屬於 2021 年的欄位外、其他沒有資料的三個人還要填入缺失值、處理手續較多。
一言以蔽之,在好豪的經驗中:
- 資料需要放在 Excel 給人類手動輸入內容、編輯表格時,常會轉換成寬資料
- 如果要開始繪圖、製作圖表,轉換成長資料來思考,使用大多數繪圖工具會更流暢
以下將教學 Python 的 Pandas 如何讓資料在長寬之間互相轉換。
轉換到長資料
我們將學習如何將上圖所示的寬資料轉換為長資料。
需要被轉換的寬資料範例:
>>> import pandas as pd
>>> wide_data = pd.DataFrame({
... 'Name': ['Emily', 'Wilson', 'John', 'Cherry'],
... '2018': [80, 83, 75, 95 ],
... '2019': [100, 95, 97, 96 ],
... '2020': [98, 95, 70, 88 ]
... })
>>> wide_data
Name 2018 2019 2020
0 Emily 80 100 98
1 Wilson 83 95 95
2 John 75 97 70
3 Cherry 95 96 88
stack()
在我們的範例寬資料裡,每個欄位名稱的「年份」本身應該是一個變數值,我們不希望它被分散在多個欄位,這裡我們使用 stack()
來處理。
stack()
會將索引(index)以外的所有欄位都轉換成一個 Series
,舉例而言,範例資料有四個人、三年的成績資料,DataFrame
總共有 12 個資料點,所以我們期望轉換後得到長度為 12 的 Series
,並且看到原本分散在多個欄位的年份資料、會被 stack()
推進索引中。
此外,為了後續更好操作,我們可以再用 reset_index()
將 Series
資料轉換回 DataFrame
。
>>> wide_data.set_index('Name')
2018 2019 2020
Name
Emily 80 100 98
Wilson 83 95 95
John 75 97 70
Cherry 95 96 88
>>> wide_data.set_index('Name').stack()
Name
Emily 2018 80
2019 100
2020 98
Wilson 2018 83
2019 95
2020 95
John 2018 75
2019 97
2020 70
Cherry 2018 95
2019 96
2020 88
dtype: int64
>>> wide_data.set_index('Name').stack().reset_index()
Name level_1 0
0 Emily 2018 80
1 Emily 2019 100
2 Emily 2020 98
3 Wilson 2018 83
4 Wilson 2019 95
5 Wilson 2020 95
6 John 2018 75
7 John 2019 97
8 John 2020 70
9 Cherry 2018 95
10 Cherry 2019 96
11 Cherry 2020 88
注意!使用 stack()
要很小心欄位的內容,stack()
會轉換索引以外的所有欄位,如果 DataFrame
內存在不想被轉換的欄位,請記得先移除那些欄位、或是設定成索引。以下是沒有注意欄位內容直接使用 stack()
的範例結果:
>>> wide_data.stack()
0 Name Emily
2018 80
2019 100
2020 98
1 Name Wilson
2018 83
2019 95
2020 95
2 Name John
2018 75
2019 97
2020 70
3 Name Cherry
2018 95
2019 96
2020 88
dtype: object
沒有正確的設定索引,stack()
讓 Name
也變成 Series
的一部份了,原本只有 12 筆成績資料,因為姓名也被轉換入長資料,結果錯誤地得出 16 筆資料的結果。
melt()
melt()
能做到的事情跟 stack()
幾乎相同,然而 melt()
可以更加靈活地運用,差別在於對表格格式的要求不同、以及函式要求參數不同的可讀性差異。
melt()
會忽略索引中的值、並且要求使用者用參數設定明確寫出重塑哪些欄位:
id_vars
:指定不要進行重塑的欄位value_vars
:指定哪些欄位要重塑成單一欄位
在範例中,姓名是不重塑的欄位、需要重塑為長資料的是年份欄位,所以我們程式這樣寫:
>>> wide_data.melt(id_vars=['Name'],
... value_vars=['2018', '2019', '2020'])
Name variable value
0 Emily 2018 80
1 Wilson 2018 83
2 John 2018 75
3 Cherry 2018 95
4 Emily 2019 100
5 Wilson 2019 95
6 John 2019 97
7 Cherry 2019 96
8 Emily 2020 98
9 Wilson 2020 95
10 John 2020 70
11 Cherry 2020 88
## 或者,因為只有 'Name' 是不重塑的欄位、其他都要重塑
## 所以可以這樣簡寫
>>> wide_data.melt(id_vars='Name')
Name variable value
0 Emily 2018 80
1 Wilson 2018 83
2 John 2018 75
3 Cherry 2018 95
4 Emily 2019 100
5 Wilson 2019 95
6 John 2019 97
7 Cherry 2019 96
8 Emily 2020 98
9 Wilson 2020 95
10 John 2020 70
11 Cherry 2020 88
這樣的語法是不是相當可讀呢?就是因為可讀性,比起 stack()
,筆者更愛用 melt()
。只是,明確寫出哪些欄位要重塑時,請注意有沒有少選了幾個欄位,否則會輸出短少的資料喔:
## 少寫了一個 '2020' 欄位的話...
>>> wide_data.melt(id_vars=['Name'],
... value_vars=['2018', '2019'])
Name variable value
0 Emily 2018 80
1 Wilson 2018 83
2 John 2018 75
3 Cherry 2018 95
4 Emily 2019 100
5 Wilson 2019 95
6 John 2019 97
7 Cherry 2019 96
pd.wide_to_long()
以上兩個函式,適用於欄位內只有一個變數,例如只有年份 “2019”。如果寬資料的欄位內有多個變數,例如考試科目加上年份 “math_2019″,你就需要使用 pd.wide_to_long()
。例如以下範例資料:
>>> wide_data_2 = pd.DataFrame({
... 'Name': ['Emily', 'Wilson', 'John', 'Cherry'],
... 'math_2018': [80, 83, 75, 95 ],
... 'math_2019': [100, 95, 97, 96 ],
... 'english_2019': [92, 95, 72, 88 ],
... 'english_2020': [98, 89, 70, 100 ]
... })
>>> wide_data_2
Name math_2018 math_2019 english_2019 english_2020
0 Emily 80 100 92 98
1 Wilson 83 95 95 89
2 John 75 97 72 70
3 Cherry 95 96 88 100
此函式會用到的參數包括:
stubnames
:要重塑的欄位名稱之字首i
:不堆疊的欄位名稱j
:重塑後的新欄位名稱sep
:分隔符號suffix
: 要重塑的欄位名稱之字尾,使用 正則表示式(RegExp)
乍看之下很複雜,其實 pd.wide_to_long()
的參數主要原則是要將欄位的字串拆解成字首、分隔符號、以及字尾,就以 “math_2019” 這個欄位為例:
- “math” 是字首
- “_” 是分隔符號
- “2019” 是字尾
>>> pd.wide_to_long(wide_data_2, # 資料
... stubnames=['math', 'english'], # 字首
... i=['Name'], # 不重塑
... j='Year', # 重塑後新欄位名稱
... sep='_', # 分隔符號
... suffix=r'\d+') # 字尾
math english
Name Year
Cherry 2018 95.0 NaN
2019 96.0 88.0
2020 NaN 100.0
Emily 2018 80.0 NaN
2019 100.0 92.0
2020 NaN 98.0
John 2018 75.0 NaN
2019 97.0 72.0
2020 NaN 70.0
Wilson 2018 83.0 NaN
2019 95.0 95.0
2020 NaN 89.0
範例資料中,每個考試科目紀錄成績的年份不同,沒有該科目的年份,pd.wide_to_long()
會自動補上 NaN
缺失值。
參數中 suffix
的 r'\d+'
是正則表示式(RegExp)的寫法,如果沒學過的讀者,請參考我以前分享過的 freeCodeCamp 正則表示式學習資源。
轉換到寬資料
接下來,學習將長資料轉換成寬資料。以下要學的兩個函式都可以說是剛剛學到的 stack()
與 melt()
的反向操作,已經學會寬轉換到長,現在要學長轉寬就不難了。
需要被轉換的長資料範例:
>>> long_data = pd.DataFrame({
... 'Name': ['Emily', 'Emily', 'Emily', 'Wilson', 'Wilson', 'Wilson', 'John', 'John', 'John', 'Cherry', 'Cherry', 'Cherry'],
... 'Year': [2018, 2019, 2020, 2018, 2019, 2020, 2018, 2019, 2020, 2018, 2019, 2020],
... 'Score': [80, 100, 98, 83, 95, 95, 75, 97, 70, 95, 96, 88]
... })
>>> long_data
Name Year Score
0 Emily 2018 80
1 Emily 2019 100
2 Emily 2020 98
3 Wilson 2018 83
4 Wilson 2019 95
5 Wilson 2020 95
6 John 2018 75
7 John 2019 97
8 John 2020 70
9 Cherry 2018 95
10 Cherry 2019 96
11 Cherry 2020 88
unstack()
unstack()
會取用最內層索引中的值,將它們轉換回寬資料的欄位名稱。也因為 unstack()
是對索引操作,所以以下範例我們需要先用 set_index()
設定索引。
>>> long_data.set_index(['Name', 'Year']).unstack()
Score
Year 2018 2019 2020
Name
Cherry 95 96 88
Emily 80 100 98
John 75 97 70
Wilson 83 95 95
使用 unstack()
要非常小心索引操作,轉換成寬資料的預設只有最內層的索引值。讀者可以自己玩玩看以下幾種設定索引的其他範例,看看會得出與預期相同、還是不同的結果:
long_data.set_index('Name').unstack()
long_data.set_index(['Year', 'Name']).unstack()
long_data.set_index(['Year', 'Name']).unstack().transpose()
unstack()
也不是只能把最內層索引轉換成寬資料,也可以用 level
參數自己設定要用哪一層索引重塑資料,請參考 pandas 官方文件說明。
如果對多層索引(MultiIndex)概念不熟悉,請參閱 pandas 的 MultiIndex 官方說明,或者《《Pandas 資料清理、重塑、過濾、視覺化》》書中的第九章有清楚的操作教學。多層索引在使用 pandas 的 groupby
操作時尤其常見,請有志於精通 pandas 的讀者務必學一下。
pivot() 與 pivot_table()
pivot()
原本的功能是進行樞紐分析,我們在此也可以把它運用在長資料轉為寬資料。其參數包括:
index
:不重塑的欄位columns
:指定欄位的值將成為轉換後的寬資料欄位名稱values
:要重塑的欄位
>>> long_data.pivot(index='Name',
... columns='Year',
... values='Score')
Year 2018 2019 2020
Name
Cherry 95 96 88
Emily 80 100 98
John 75 97 70
Wilson 83 95 95
以上的範例程式,用 pivot_table()
會得到一模一樣的結果,請讀者試試 long_data.pivot_table(index='Name', columns='Year', values='Score')
。那麼 pivot()
與 pivot_table()
有什麼不同呢?
pivot_table()
是 pivot()
的通用版本。使用 pivot()
時有額外的限制:原始長資料內,要重複的幾個欄位、不可以有重複的值,我們直接看以下使用 pivot()
會產生錯誤的簡單範例:
>>> long_data_2 = pd.DataFrame({
... 'Name': ['Emily', 'Emily', 'Emily', 'Emily', 'Wilson', 'Wilson'],
... 'Year': [2018, 2019, 2020, 2020, 2018, 2019],
... 'Score': [80, 100, 98, 91, 83, 95]
... })
>>> long_data_2
Name Year Score
0 Emily 2018 80
1 Emily 2019 100
2 Emily 2020 98 # Emily 在 2020 年
3 Emily 2020 91 # 有兩筆考試成績!
4 Wilson 2018 83
5 Wilson 2019 95
>>> long_data_2.pivot(index='Name',
... columns='Year',
... values='Score')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
... 中間錯誤訊息省略 ...
raise ValueError("Index contains duplicate entries, cannot reshape")
ValueError: Index contains duplicate entries, cannot reshape
以上範例資料中,Emily 在 2020 的考試成績有兩筆,在長資料可以儲存成兩列資料、毫無問題,但是重塑成寬資料後,Emily 在 2020 考試成績資料在表格內只有「一格」的儲存空間,我們在程式裡也沒說怎麼把多筆資料塞進「同一格」,所以 pivot()
會出現錯誤訊息。
這時 pivot_table()
就可以發揮威力了。pivot_table()
有 aggfunc
參數,讓使用者寫清楚如果「同一格」有多筆資料,我們該做什麼。例如,既然 Emily 在 2020 有多次考試成績,我們自己指定重塑後,該學生該年資料就取平均值,如此我們終於能成功轉換成寬資料了:
>>> import numpy as np
>>> long_data_2.pivot_table(index='Name',
... columns='Year',
... values='Score',
... aggfunc=np.mean)
Year 2018 2019 2020
Name
Emily 80.0 100.0 94.5
Wilson 83.0 95.0 NaN
pivot()
跟 pivot_table()
長寬資料轉換功能相似,該用哪一個呢?筆者好豪喜歡優先使用 pivot()
,在單純的長寬資料重塑任務中,我通常希望資料只是改變形狀、不想要資料「被計算」,而 pivot_table()
的 aggfunc
預設會取平均值,如果遇到與上面相似、同個學生在同一年有多筆成績資料的狀況,pivot_table()
預設取平均值會害我忽略「資料重複」這件事!所以我通常會優先使用 pivot()
,讓程式在必要的時候用錯誤訊息提醒我:有重複資料、請小心重塑。
結語
相信讀者即使認真讀完這篇筆記也會有疑問:相同的長寬資料轉換,在 pandas 內可以用不同函式來達成,我到底該用哪一個?
好豪個人建議:你可以依照你是否習慣用索引還是欄位操作來選擇,你可以參考自己常用的資料表合併是哪個函式。
如果你喜歡使用 join()
來合併表格,此函式是用索引來合併的,那就建議你:
- 使用
stack()
將資料由寬轉為長 - 使用
unstack()
將資料由長轉為寬
而如果你常用 merge()
來合併表格,此函式是用欄位來合併的,建議你:
- 使用
melt()
將資料由寬轉為長 - 使用
pivot()
將資料由長轉為寬
一致都用索引、或者一致都用欄位來轉換資料,就不用一直在 reset_index()
或 set_index()
之間來回反覆操作囉!
這篇筆記來自於我在 《Pandas 資料清理、重塑、過濾、視覺化》 書中所學,我在這本書學會許多進階操作,筆者過去求職的 上機程式考試 遇到過的 pandas 操作困難,也是看了這本書才找到解答,所以我相信不論是想精進資料分析實戰、或者準備求職的讀者,都值得花時間用這本書進一步修煉。
你正在學習 Pandas 嗎?好豪還寫了其他 Pandas 學習心得與技巧分享,我相信會對你有幫助:
最後,如果這篇文章有幫助到你,歡迎追蹤好豪的 Facebook 粉絲專頁,我會持續分享 Pandas 與 Python 的學習筆記;也可以點選下方按鈕,分享給對正在精進資料科學的朋友們。