1. 程式人生 > 實用技巧 >【12月DW打卡】joyful-pandas - 05 - joyful-pandas - pandas變形 (長寬表、pivot、melt、wide_to_long、stack與unstack)

【12月DW打卡】joyful-pandas - 05 - joyful-pandas - pandas變形 (長寬表、pivot、melt、wide_to_long、stack與unstack)

第五章 pandas變形

import numpy as np
import pandas as pd

一、長寬表的變形

  • 舉例性別列,用一列儲存為長表,用兩列(男、女)分別儲存為寬表
  • pandas針對此類長寬表的變形操作設計了一些有關的變形函式。
  • jupyter nbconvert --to markdown E:\PycharmProjects\TianChiProject\00_山楓葉紛飛\competitions\008_joyful-pandas\05_pandas變形.ipynb
# 長
pd.DataFrame({'Gender':['F','F','M','M'], 'Height':[163, 160, 175, 180]})
Gender Height
0 F 163
1 F 160
2 M 175
3 M 180
# 寬
pd.DataFrame({'Height: F':[163, 160], 'Height: M':[175, 180]})
Height: F Height: M
0 163 175
1 160 180

1. pivot

pivot是一種典型的長表變寬表的函式,首先來看一個例子:下表儲存了張三和李四的語文和數學分數,現在想要把語文和數學分數作為列來展示。

df = pd.DataFrame({'Class':[1,1,2,2],
                   'Name':['San Zhang','San Zhang','Si Li','Si Li'],
                   'Subject':['Chinese','Math','Chinese','Math'],
                   'Grade':[80,75,90,85]})
df
Class Name Subject Grade
0 1 San Zhang Chinese 80
1 1 San Zhang Math 75
2 2 Si Li Chinese 90
3 2 Si Li Math 85

對於一個基本的長變寬的操作而言,最重要的有三個要素,分別是變形後的行索引、需要轉到列索引的列,以及這些列和行索引對應的數值,它們分別對應了pivot方法中的index, columns, values引數。

  • 新生成表的列索引是columns對應列的unique值,而新表的行索引是index對應列的unique值,而values對應了想要展示的數值列。
  • 確保行*列的所有組合情況中沒有重複值,存在重複值會提示reshape異常,pivot無法進行繪製。
df.pivot(index='Name', columns='Subject', values='Grade')

Subject Chinese Math
Name
San Zhang 80 75
Si Li 90 85
# 嘗試進行一次報錯處理
df.loc[1, 'Subject'] = 'Chinese'
try:
    df.pivot(index='Name', columns='Subject', values='Grade')
except Exception as e:
    Err_Msg = e
Err_Msg

ValueError('Index contains duplicate entries, cannot reshape')

pandas從1.1.0開始,pivot相關的三個引數允許被設定為列表,這也意味著會返回多級索引。

這裡構造一個相應的例子來說明如何使用:下表中六列分別為班級、姓名、測試型別(期中考試和期末考試)、科目、成績、排名。

df = pd.DataFrame({'Class':[1, 1, 2, 2, 1, 1, 2, 2],
                   'Name':['San Zhang', 'San Zhang', 'Si Li', 'Si Li',
                              'San Zhang', 'San Zhang', 'Si Li', 'Si Li'],
                   'Examination': ['Mid', 'Final', 'Mid', 'Final',
                                    'Mid', 'Final', 'Mid', 'Final'],
                   'Subject':['Chinese', 'Chinese', 'Chinese', 'Chinese',
                                 'Math', 'Math', 'Math', 'Math'],
                   'Grade':[80, 75, 85, 65, 90, 85, 92, 88],
                   'rank':[10, 15, 21, 15, 20, 7, 6, 2]})
df.head()

Class Name Examination Subject Grade rank
0 1 San Zhang Mid Chinese 80 10
1 1 San Zhang Final Chinese 75 15
2 2 Si Li Mid Chinese 85 21
3 2 Si Li Final Chinese 65 15
4 1 San Zhang Mid Math 90 20
# 現在想要把測試型別和科目聯合組成的四個類別(期中語文、期末語文、期中數學、期末數學)轉到列索引,並且同時統計成績和排名:
pivot_multi = df.pivot(index = ['Class', 'Name'],
                       columns = ['Subject','Examination'],
                       values = ['Grade','rank'])
pivot_multi
Grade rank
Subject Chinese Math Chinese Math
Examination Mid Final Mid Final Mid Final Mid Final
Class Name
1 San Zhang 80 75 90 85 10 15 20 7
2 Si Li 85 65 92 88 21 15 6 2

根據唯一性原則,(pivot)新表的行索引等價於對index中的多列使用drop_duplicates,而列索引的長度為values中的元素個數乘以columns的唯一組合數量(與index類似) 。

  1. pivot_table

pivot的使用依賴於唯一性條件,那如果不滿足唯一性條件,那麼必須通過聚合操作使得相同行列組合對應的多個值變為一個值。

例如,張三和李四都參加了兩次語文考試和數學考試,按照學院規定,最後的成績是兩次考試分數的平均值,此時就無法通過pivot函式來完成。

df = pd.DataFrame({'Name':['San Zhang', 'San Zhang',
                              'San Zhang', 'San Zhang',
                              'Si Li', 'Si Li', 'Si Li', 'Si Li'],
                   'Subject':['Chinese', 'Chinese', 'Math', 'Math',
                                 'Chinese', 'Chinese', 'Math', 'Math'],
                   'Grade':[80, 90, 100, 90, 70, 80, 85, 95]})
df
Name Subject Grade
0 San Zhang Chinese 80
1 San Zhang Chinese 90
2 San Zhang Math 100
3 San Zhang Math 90
4 Si Li Chinese 70
5 Si Li Chinese 80
6 Si Li Math 85
7 Si Li Math 95

pandas中提供了pivot_table來實現,其中的aggfunc引數就是使用的聚合函式。上述場景可以如下寫出:

df.pivot_table(index = 'Name',
               columns = 'Subject',
               values = 'Grade',
               aggfunc = 'mean')

Subject Chinese Math
Name
San Zhang 85 95
Si Li 75 90

此外,pivot_table具有邊際彙總的功能,可以通過設定margins=True來實現,其中邊際的聚合方式與aggfunc中給出的聚合方法一致。下面就分別統計了語文均分和數學均分、張三均分和李四均分,以及總體所有分數的均分:

df.pivot_table(index = 'Name',
               columns = 'Subject',
               values = 'Grade',
               aggfunc='mean',
               margins=True)

Subject Chinese Math All
Name
San Zhang 85 95.0 90.00
Si Li 75 90.0 82.50
All 80 92.5 86.25

【練一練】

在上面的邊際彙總例子中,行或列的彙總為新表中行元素或者列元素的平均值,而總體的彙總為新表中四個元素的平均值。這種關係一定成立嗎?若不成立,請給出一個例子來說明。

df_err = pd.DataFrame({'Name':['San Zhang', 'San Zhang',
                              'San Zhang', 'San Zhang',
                              'Si Li', 'Si Li', 'Si Li', 'Si Li'],
                   'Subject':['Chinese', 'Chinese', 'Math', 'Math',
                                 'Chinese', 'Chinese', 'Math', 'Math'],
                   'Grade':[80, None, 100, 90, 70, 80, 85, 95]})
df_err.pivot_table(index = 'Name',
               columns = 'Subject',
               values = 'Grade',
               aggfunc='mean',
               margins=True)

Subject Chinese Math All
Name
San Zhang 80.000000 95.0 90.000000
Si Li 75.000000 90.0 82.500000
All 76.666667 92.5 85.714286
(80.000000+75.00000+95+90)/4.0
85.0

不一定,部分元素為None時,會引起總數的變化,求mean時會剔除掉None。
在圖中可以看到,Chinese中的第一列的All的值為76/67 != (80+75)/2

[end]

  1. melt (熔化,把寬錶轉為長表)

在下面的例子中,Subject以列索引的形式儲存,現在想要將其壓縮到一個列中。

df = pd.DataFrame({'Class':[1,2],
                   'Name':['San Zhang', 'Si Li'],
                   # 有兩門學科,Grade分類為Chinese或者math
                   'Chinese':[80, 90],
                   'Math':[80, 75]})
df
Class Name Chinese Math
0 1 San Zhang 80 80
1 2 Si Li 90 75
df_melted=df.melt(id_vars = ['Class', 'Name'],
                    value_vars = ['Chinese', 'Math'],
                    var_name = 'Subject',
                    value_name = 'Grade')
df_melted
Class Name Subject Grade
0 1 San Zhang Chinese 80
1 2 Si Li Chinese 90
2 1 San Zhang Math 80
3 2 Si Li Math 75

melt和pivot是一組互逆過程,那麼就一定可以通過pivot操作把df_melted轉回df的形式:

df_unmelted = df_melted.pivot(index = ['Class', 'Name'],
                              columns='Subject',
                              values='Grade')

df_unmelted
Subject Chinese Math
Class Name
1 San Zhang 80 80
2 Si Li 90 75
df_unmelted_reset_index = df_unmelted.reset_index().rename_axis(columns={'Subject':''})
# df_unmelted.equals(df)
df_unmelted_reset_index
Class Name Chinese Math
0 1 San Zhang 80 80
1 2 Si Li 90 75
df_unmelted.equals(df)
False
  1. wide_to_long

elt 方法中,在列索引中被壓縮的一組值對應的列元素只能代表同一層次的含義,即 values_name 。
現在如果列中包含了交叉類別,比如期中期末的類別和語文數學的類別,那麼想要把 values_name 對應的 Grade 擴充為兩列分別對應語文分數和數學分數,只把期中期末的資訊壓縮,這種需求下就要使用 wide_to_long 函式來完成。

df = pd.DataFrame({'Class':[1,2],'Name':['San Zhang', 'Si Li'],
                   'Chinese_Mid':[80, 75], 'Math_Mid':[90, 85],
                   'Chinese_Final':[80, 75], 'Math_Final':[90, 85]})
df
Class Name Chinese_Mid Math_Mid Chinese_Final Math_Final
0 1 San Zhang 80 90 80 90
1 2 Si Li 75 85 75 85
pd.wide_to_long(df,
                # stubnames表示轉換後的列名稱
                stubnames=['Chinese', 'Math'],
                # 索引列的名稱
                i = ['Class', 'Name'],
                # 壓縮到每行的變數名含義
                j='Examination',
                sep='_',
                suffix='.+')
Chinese Math
Class Name Examination
1 San Zhang Mid 80 90
Final 80 90
2 Si Li Mid 75 85
Final 75 85

更多見下面的解析圖片

下面給出一個比較複雜的案例,把之前在pivot一節中多列操作的結果(產生了多級索引),利用wide_to_long函式,將其轉為原來的形態。其中,使用了第八章的str.split函式,目前暫時只需將其理解為對序列按照某個分隔符進行拆分即可。

# 例子
res = pivot_multi.copy()
res.columns = res.columns.map(lambda x:'_'.join(x))
res = res.reset_index()
res = pd.wide_to_long(res, stubnames=['Grade', 'rank'],
                           i = ['Class', 'Name'],
                           j = 'Subject_Examination',
                           sep = '_',
                           suffix = '.+')
res
Grade rank
Class Name Subject_Examination
1 San Zhang Chinese_Mid 80 10
Chinese_Final 75 15
Math_Mid 90 20
Math_Final 85 7
2 Si Li Chinese_Mid 85 21
Chinese_Final 65 15
Math_Mid 92 6
Math_Final 88 2
# 續上
res = res.reset_index()
res[['Subject', 'Examination']] = res['Subject_Examination'].str.split('_', expand=True)
res = res[['Class', 'Name', 'Examination', 'Subject', 'Grade', 'rank']].sort_values('Subject')
res = res.reset_index(drop=True)
res
Class Name Examination Subject Grade rank
0 1 San Zhang Mid Chinese 80 10
1 1 San Zhang Final Chinese 75 15
2 2 Si Li Mid Chinese 85 21
3 2 Si Li Final Chinese 65 15
4 1 San Zhang Mid Math 90 20
5 1 San Zhang Final Math 85 7
6 2 Si Li Mid Math 92 6
7 2 Si Li Final Math 88 2

二、索引的變形

1. stack與unstack

在第二章中提到了利用swaplevel或者reorder_levels進行索引內部的層交換,下面就要討論\(\color{red}{行列索引之間}\)的交換,由於這種交換帶來了DataFrame維度上的變化,因此屬於變形操作。

在第一節中提到的4種變形函式與其不同之處在於,它們都屬於某一列或幾列\(\color{red}{元素}\)\(\color{red}{列索引}\)之間的轉換,而不是索引之間的轉換。

unstack函式的作用是把行索引轉為列索引,例如下面這個簡單的例子:

df = pd.DataFrame(np.ones((4,2)),
                  index = pd.Index([('A', 'cat', 'big'),
                                    ('A', 'dog', 'small'),
                                    ('B', 'cat', 'big'),
                                    ('B', 'dog', 'small')]),
                  columns=['col_1', 'col_2'])
df
col_1 col_2
A cat big 1.0 1.0
dog small 1.0 1.0
B cat big 1.0 1.0
dog small 1.0 1.0
df.unstack().unstack()


col_1 col_2
big small big small
cat dog cat dog cat dog cat dog
A 1.0 NaN NaN 1.0 1.0 NaN NaN 1.0
B 1.0 NaN NaN 1.0 1.0 NaN NaN 1.0

unstack的主要引數是移動的層號,預設轉化最內層,移動到列索引的最內層,同時支援同時轉化多個層:

df.unstack([0,2])

col_1 col_2
A B A B
big small big small big small big small
cat 1.0 NaN 1.0 NaN 1.0 NaN 1.0 NaN
dog NaN 1.0 NaN 1.0 NaN 1.0 NaN 1.0

類似於pivot中的唯一性要求,在unstack中必須保證\(\color{red}{被轉為列索引的行索引層}\)\(\color{red}{被保留的行索引層}\)構成的組合是唯一的,例如把前兩個列索引改成相同的破壞唯一性,那麼就會報錯:

my_index = df.index.to_list()
my_index[1] = my_index[0]
df.index = pd.Index(my_index)
df
col_1 col_2
A cat big 1.0 1.0
big 1.0 1.0
B cat big 1.0 1.0
dog small 1.0 1.0
try:
    df.unstack()
except Exception as e:
    Err_Msg = e
Err_Msg


ValueError('Index contains duplicate entries, cannot reshape')

unstack相反,stack的作用就是把列索引的層壓入行索引,其用法完全類似。

2. 聚合與變形的關係

  • 聚合:分組聚合操作,由於生成了新的行列索引,因此必然也屬於某種特殊的變形操作,但由於聚合之後把原來的多個值變為了一個值,因此values的個數產生了變化
  • 變形:除了帶有聚合效果的pivot_table以外,所有的函式在變形前後並不會帶來values個數的改變,只是這些值在呈現的形式上發生了變化

三、其他變形函式

1. crosstab

crosstab並不是一個值得推薦使用的函式,因為它能實現的所有功能pivot_table都能完成,並且速度更快。

在預設狀態下,crosstab可以統計元素組合出現的頻數,即count操作。例如統計learn_pandas資料集中學校和轉系情況對應的頻數:

df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\learn_pandas.csv')
pd.crosstab(index = df.School, columns = df.Transfer)
Transfer N Y
School
Fudan University 38 1
Peking University 28 2
Shanghai Jiao Tong University 53 0
Tsinghua University 62 4

同樣,可以利用pivot_table進行等價操作,由於這裡統計的是組合的頻數,因此values引數無論傳入哪一個列都不會影響最後的結果:

df.pivot_table(index = 'School',
               columns = 'Transfer',
               values = 'Name',
               aggfunc = 'count')
Transfer N Y
School
Fudan University 38.0 1.0
Peking University 28.0 2.0
Shanghai Jiao Tong University 53.0 NaN
Tsinghua University 62.0 4.0

【練一練】

前面提到了crosstab的效能劣於pivot_table,請選用多個聚合方法進行驗證。

使用
%timeit -n 100 ___ 即可

2. explode

explode引數能夠對某一列的元素進行縱向的展開,被展開的單元格必須儲存list, tuple, Series, np.ndarray中的一種型別。

df_ex = pd.DataFrame({'A': [[1, 2], 'my_str', {1, 2}, pd.Series([3, 4])],
                      'B': 1})
df_ex
A B
0 [1, 2] 1
1 my_str 1
2 {1, 2} 1
3 0 3 1 4 dtype: int64 1
df_ex.explode('A')
A B
0 1 1
0 2 1
1 my_str 1
2 {1, 2} 1
3 3 1
3 4 1
  1. get_dummies (虛擬向量,ont-hot)

get_dummies是用於特徵構建的重要函式之一,其作用是把類別特徵轉為指示變數。例如,對年級一列轉為指示變數,屬於某一個年級的對應列標記為1,否則為0:

pd.get_dummies(df.Grade).head()
Freshman Junior Senior Sophomore
0 1 0 0 0
1 1 0 0 0
2 0 0 1 0
3 0 0 0 1
4 0 0 0 1

四、練習

Ex1:美國非法藥物資料集

現有一份關於美國非法藥物的資料集,其中SubstanceName, DrugReports分別指藥物名稱和報告數量:

df = pd.read_csv('E:\\PycharmProjects\\DatawhaleChina\\joyful-pandas\\data\\drugs.csv').sort_values(['State','COUNTY','SubstanceName'],ignore_index=True)
df.head(3)
YYYY State COUNTY SubstanceName DrugReports
0 2011 KY ADAIR Buprenorphine 3
1 2012 KY ADAIR Buprenorphine 5
2 2013 KY ADAIR Buprenorphine 4
  1. 將資料轉為如下的形式:
df_pivot = df.pivot_table(index=['State','COUNTY','SubstanceName'],
               columns = 'YYYY',
               values = 'DrugReports'
                          ).rename_axis(columns={'YYYY':''})
df_pivot
2010 2011 2012 2013 2014 2015 2016 2017
State COUNTY SubstanceName
KY ADAIR Buprenorphine NaN 3.0 5.0 4.0 27.0 5.0 7.0 10.0
Codeine NaN NaN 1.0 NaN NaN NaN NaN 1.0
Fentanyl NaN NaN 1.0 NaN NaN NaN NaN NaN
Heroin NaN NaN 1.0 2.0 NaN 1.0 NaN 2.0
Hydrocodone 6.0 9.0 10.0 10.0 9.0 7.0 11.0 3.0
... ... ... ... ... ... ... ... ... ... ...
WV WOOD Oxycodone 6.0 4.0 24.0 7.0 7.0 11.0 7.0 1.0
Tramadol NaN NaN NaN NaN 1.0 NaN 4.0 3.0
WYOMING Buprenorphine NaN 1.0 1.0 1.0 NaN NaN NaN 1.0
Hydrocodone 1.0 5.0 NaN NaN 1.0 NaN 1.0 NaN
Oxycodone 5.0 4.0 14.0 12.0 5.0 NaN NaN NaN

6214 rows × 8 columns

df_pivot = df_pivot.reset_index()
df_melt = df_pivot.melt(
    id_vars=['State','COUNTY','SubstanceName'],
                    value_vars = df_pivot.columns[-8:],
                    var_name = 'YYYY',
                    value_name='DrugReports')
df_melt = df_melt.dropna(subset=['DrugReports'])
df_melt
State COUNTY SubstanceName YYYY DrugReports
4 KY ADAIR Hydrocodone 2010 6.0
6 KY ADAIR Methadone 2010 1.0
13 KY ALLEN Hydrocodone 2010 10.0
15 KY ALLEN Methadone 2010 4.0
17 KY ALLEN Oxycodone 2010 15.0
... ... ... ... ... ...
49702 WV WOOD Hydrocodone 2017 8.0
49704 WV WOOD Isobutyryl fentanyl 2017 3.0
49707 WV WOOD Oxycodone 2017 1.0
49708 WV WOOD Tramadol 2017 3.0
49709 WV WYOMING Buprenorphine 2017 1.0

24062 rows × 5 columns

  1. 將第1問中的結果恢復為原表。
df_melt= df_melt[df.columns].sort_values(['State','COUNTY','SubstanceName'],ignore_index=True).astype({'YYYY':'int64', 'DrugReports':'int64'})
df_melt.equals(df)

True
  1. State分別統計每年的報告數量總和,其中State, YYYY分別為列索引和行索引,要求分別使用pivot_table函式與groupby+unstack兩種不同的策略實現,並體會它們之間的聯絡。
# 策略一
res = df.pivot_table(index='YYYY', columns='State', values='DrugReports', aggfunc='sum')
res

State KY OH PA VA WV
YYYY
2010 10453 19707 19814 8685 2890
2011 10289 20330 19987 6749 3271
2012 10722 23145 19959 7831 3376
2013 11148 26846 20409 11675 4046
2014 11081 30860 24904 9037 3280
2015 9865 37127 25651 8810 2571
2016 9093 42470 26164 10195 2548
2017 9394 46104 27894 10448 1614
# 策略二
df.groupby(['State', 'YYYY'])['DrugReports'].sum().to_frame().unstack(0).droplevel(0, axis=1)

State KY OH PA VA WV
YYYY
2010 10453 19707 19814 8685 2890
2011 10289 20330 19987 6749 3271
2012 10722 23145 19959 7831 3376
2013 11148 26846 20409 11675 4046
2014 11081 30860 24904 9037 3280
2015 9865 37127 25651 8810 2571
2016 9093 42470 26164 10195 2548
2017 9394 46104 27894 10448 1614

Ex2:特殊的wide_to_long方法
從功能上看, melt 方法應當屬於 wide_to_long 的一種特殊情況,即 stubnames 只有一類。請使用 wide_to_long 生成 melt 一節中的 df_melted 。(提示:對列名增加適當的字首)

df = pd.DataFrame({'Class':[1,2],
                   'Name':['San Zhang', 'Si Li'],
                   'Chinese':[80, 90],
                   'Math':[80, 75]})
df


Class Name Chinese Math
0 1 San Zhang 80 80
1 2 Si Li 90 75
df_melted=df.melt(id_vars = ['Class', 'Name'],
                    value_vars = ['Chinese', 'Math'],
                    var_name = 'Subject',
                    value_name = 'Grade')
df_melted
Class Name Subject Grade
0 1 San Zhang Chinese 80
1 2 Si Li Chinese 90
2 1 San Zhang Math 80
3 2 Si Li Math 75
# 使用 wide_to_long 生成 melt 一節中的 df_melted
df_wtl = df.rename(columns={'Chinese':'my_Chinese', 'Math':'my_Math'})
df_wtl = pd.wide_to_long(df_wtl,
                stubnames=['my'],
                i = ['Class', 'Name'],
                j='Subject',
                sep='_',
                suffix='.+').reset_index().rename(columns={'my':'Grade'})
df_wtl


Class Name Subject Grade
0 1 San Zhang Chinese 80
1 1 San Zhang Math 80
2 2 Si Li Chinese 90
3 2 Si Li Math 75