Python Pandas 的長資料與寬資料轉換

by 好豪
Published: Last Updated on

即使內容相同,資料可以用許多不同的形式表示,其中,長與寬資料的資料形式差異,會改變資料科學家操作資料的方便程度,長寬資料的不同尤其對製作數據圖表有非常大的影響。

這則筆記將跟你分享區分長與寬資料為何能讓你的資料科學工作更有效率,並且教學 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 缺失值。

參數中 suffixr'\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-cookbook


你正在學習 Pandas 嗎?好豪還寫了其他 Pandas 學習心得與技巧分享,我相信會對你有幫助:

最後,如果這篇文章有幫助到你,歡迎追蹤好豪的 Facebook 粉絲專頁,我會持續分享 Pandas 與 Python 的學習筆記;也可以點選下方按鈕,分享給對正在精進資料科學的朋友們。

推薦閱讀