資料基礎---《利用Python進行資料分析·第2版》第8章 資料規整:聚合、合併和重塑
之前自己對於numpy和pandas是要用的時候東學一點西一點,直到看到《利用Python進行資料分析·第2版》,覺得只看這一篇就夠了。非常感謝原博主的翻譯和分享。
在許多應用中,資料可能分散在許多檔案或資料庫中,儲存的形式也不利於分析。本章關注可以聚合、合併、重塑資料的方法。
首先,我會介紹pandas的層次化索引,它廣泛用於以上操作。然後,我深入介紹了一些特殊的資料操作。在第14章,你可以看到這些工具的多種應用。
8.1 層次化索引
層次化索引(hierarchical indexing)是pandas的一項重要功能,它使你能在一個軸上擁有多個(兩個以上)索引級別。抽象點說,它使你能以低維度形式處理高維度資料。我們先來看一個簡單的例子:建立一個Series,並用一個由列表或陣列組成的列表作為索引:
import pandas as pd
import numpy as np
data =pd.Series(np.random.randn(9),index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],[1, 2, 3, 1, 3, 1, 2, 2, 3]])
data
a 1 -0.626605 2 -0.099608 3 1.832826 b 1 0.764571 3 1.241071 c 1 -1.656578 2 -0.837847 d 2 -1.563933 3 1.566208 dtype: float64
看到的結果是經過美化的帶有MultiIndex索引的Series的格式。索引之間的“間隔”表示“直接使用上面的標籤”:
data.index
MultiIndex(levels=[['a', 'b', 'c', 'd'], [1, 2, 3]],
labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 0, 2, 0, 1, 1, 2]])
對於一個層次化索引的物件,可以使用所謂的部分索引,使用它選取資料子集的操作更簡單:
data['b']
1 0.764571 3 1.241071 dtype: float64
data['b':'c']
b 1 0.764571
3 1.241071
c 1 -1.656578
2 -0.837847
dtype: float64
data.loc[['b','c']]
b 1 0.764571
3 1.241071
c 1 -1.656578
2 -0.837847
dtype: float64
有時甚至還可以在“內層”中進行選取:
data.loc[:,2]
a -0.099608
c -0.837847
d -1.563933
dtype: float64
這裡是Series,[:,2]中逗號的兩邊都是表示行索引,前者是外層索引,後者是內層索引
層次化索引在資料重塑和基於分組的操作(如透視表生成)中扮演著重要的角色。例如,可以通過unstack方法將這段資料重新安排到一個DataFrame中:
data.unstack()
1 | 2 | 3 | |
---|---|---|---|
a | -0.626605 | -0.099608 | 1.832826 |
b | 0.764571 | NaN | 1.241071 |
c | -1.656578 | -0.837847 | NaN |
d | NaN | -1.563933 | 1.566208 |
unstack的逆運算是stack:
data.unstack().stack()
a 1 -0.626605
2 -0.099608
3 1.832826
b 1 0.764571
3 1.241071
c 1 -1.656578
2 -0.837847
d 2 -1.563933
3 1.566208
dtype: float64
stack和unstack將在本章後面詳細講解。
對於一個DataFrame,每條軸都可以有分層索引:
frame = pd.DataFrame(np.arange(12).reshape((4, 3)), index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],columns=[['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']])
frame
Ohio | Colorado | |||
---|---|---|---|---|
Green | Red | Green | ||
a | 1 | 0 | 1 | 2 |
2 | 3 | 4 | 5 | |
b | 1 | 6 | 7 | 8 |
2 | 9 | 10 | 11 |
各層都可以有名字(可以是字串,也可以是別的Python物件)。如果指定了名稱,它們就會顯示在控制檯輸出中:
frame.index.names=['key1','key2']
frame.columns.names=['state','color']
frame
state | Ohio | Colorado | ||
---|---|---|---|---|
color | Green | Red | Green | |
key1 | key2 | |||
a | 1 | 0 | 1 | 2 |
2 | 3 | 4 | 5 | |
b | 1 | 6 | 7 | 8 |
2 | 9 | 10 | 11 |
注意:小心區分索引名state、color與行標籤。
有了部分列索引,因此可以輕鬆選取列分組:
frame['Ohio']
color | Green | Red | |
---|---|---|---|
key1 | key2 | ||
a | 1 | 0 | 1 |
2 | 3 | 4 | |
b | 1 | 6 | 7 |
2 | 9 | 10 |
可以單獨建立MultiIndex然後複用。上面那個DataFrame中的(帶有分級名稱)列可以這樣建立:
from pandas import MultiIndex
MultiIndex.from_arrays([['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']],names=['state','color'])
MultiIndex(levels=[['Colorado', 'Ohio'], ['Green', 'Red']],
labels=[[1, 1, 0], [0, 1, 0]],
names=['state', 'color'])
重排與分級排序
有時,你需要重新調整某條軸上各級別的順序,或根據指定級別上的值對資料進行排序。swaplevel接受兩個級別編號或名稱,並返回一個互換了級別的新物件(但資料不會發生變化):
frame.swaplevel('key1','key2')
state | Ohio | Colorado | ||
---|---|---|---|---|
color | Green | Red | Green | |
key2 | key1 | |||
1 | a | 0 | 1 | 2 |
2 | a | 3 | 4 | 5 |
1 | b | 6 | 7 | 8 |
2 | b | 9 | 10 | 11 |
而sort_index則根據單個級別中的值對資料進行排序。交換級別時,常常也會用到sort_index,這樣最終結果就是按照指定順序進行字母排序了:
frame.sort_index(level=1)
state | Ohio | Colorado | ||
---|---|---|---|---|
color | Green | Red | Green | |
key1 | key2 | |||
a | 1 | 0 | 1 | 2 |
b | 1 | 6 | 7 | 8 |
a | 2 | 3 | 4 | 5 |
b | 2 | 9 | 10 | 11 |
frame.swaplevel(0,1).sort_index(level=0)
state | Ohio | Colorado | ||
---|---|---|---|---|
color | Green | Red | Green | |
key2 | key1 | |||
1 | a | 0 | 1 | 2 |
b | 6 | 7 | 8 | |
2 | a | 3 | 4 | 5 |
b | 9 | 10 | 11 |
根據級別彙總統計
許多對DataFrame和Series的描述和彙總統計都有一個level選項,它用於指定在某條軸上求和的級別。再以上面那個DataFrame為例,我們可以根據行或列上的級別來進行求和:level指的是保留的索引,按這個索引來分組
frame.sum()#預設是對各列求和
state color
Ohio Green 18
Red 22
Colorado Green 26
dtype: int64
frame.sum(level='key2')#還是對列求和,只是分了組
state | Ohio | Colorado | |
---|---|---|---|
color | Green | Red | Green |
key2 | |||
1 | 6 | 8 | 10 |
2 | 12 | 14 | 16 |
frame.sum(level='color',axis=1)
color | Green | Red | |
---|---|---|---|
key1 | key2 | ||
a | 1 | 2 | 1 |
2 | 8 | 4 | |
b | 1 | 14 | 7 |
2 | 20 | 10 |
這其實是利用了pandas的groupby功能,本書稍後將對其進行詳細講解。
使用DataFrame的列進行索引
人們經常想要將DataFrame的一個或多個列當做行索引來用,或者可能希望將行索引變成DataFrame的列。以下面這個DataFrame為例:
frame = pd.DataFrame({'a': range(7), 'b': range(7, 0, -1),'c': ['one', 'one', 'one', 'two', 'two', 'two', 'two'],'d': [0, 1, 2, 0, 1, 2, 3]})
frame
a | b | c | d | |
---|---|---|---|---|
0 | 0 | 7 | one | 0 |
1 | 1 | 6 | one | 1 |
2 | 2 | 5 | one | 2 |
3 | 3 | 4 | two | 0 |
4 | 4 | 3 | two | 1 |
5 | 5 | 2 | two | 2 |
6 | 6 | 1 | two | 3 |
DataFrame的set_index函式會將其一個或多個列轉換為行索引,並建立一個新的DataFrame:
frame2=frame.set_index(['c','d'])
frame2
a | b | ||
---|---|---|---|
c | d | ||
one | 0 | 0 | 7 |
1 | 1 | 6 | |
2 | 2 | 5 | |
two | 0 | 3 | 4 |
1 | 4 | 3 | |
2 | 5 | 2 | |
3 | 6 | 1 |
預設情況下,那些列會從DataFrame中移除,但也可以將其保留下來:
frame.set_index(['c','d'],drop=False)
a | b | c | d | ||
---|---|---|---|---|---|
c | d | ||||
one | 0 | 0 | 7 | one | 0 |
1 | 1 | 6 | one | 1 | |
2 | 2 | 5 | one | 2 | |
two | 0 | 3 | 4 | two | 0 |
1 | 4 | 3 | two | 1 | |
2 | 5 | 2 | two | 2 | |
3 | 6 | 1 | two | 3 |
reset_index的功能跟set_index剛好相反,層次化索引的級別會被轉移到列裡面:
frame2.reset_index()
c | d | a | b | |
---|---|---|---|---|
0 | one | 0 | 0 | 7 |
1 | one | 1 | 1 | 6 |
2 | one | 2 | 2 | 5 |
3 | two | 0 | 3 | 4 |
4 | two | 1 | 4 | 3 |
5 | two | 2 | 5 | 2 |
6 | two | 3 | 6 | 1 |
8.2 合併資料集
pandas物件中的資料可以通過一些方式進行合併:
- pandas.merge可根據一個或多個鍵將不同DataFrame中的行連線起來。SQL或其他關係型資料庫的使用者對此應該會比較熟悉,因為它實現的就是資料庫的join操作。
- pandas.concat可以沿著一條軸將多個物件堆疊到一起。
- 例項方法combine_first可以將重複資料拼接在一起,用一個物件中的值填充另一個物件中的缺失值。
我將分別對它們進行講解,並給出一些例子。本書剩餘部分的示例中將經常用到它們。
資料庫風格的DataFrame合併
資料集的合併(merge)或連線(join)運算是通過一個或多個鍵將行連線起來的。這些運算是關係型資料庫(基於SQL)的核心。pandas的merge函式是對資料應用這些演算法的主要切入點。
以一個簡單的例子開始:
df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'], 'data1': range(7)})
df2 = pd.DataFrame({'key': ['a', 'b', 'd'], 'data2': range(3)})
df1
data1 | key | |
---|---|---|
0 | 0 | b |
1 | 1 | b |
2 | 2 | a |
3 | 3 | c |
4 | 4 | a |
5 | 5 | a |
6 | 6 | b |
df2
data2 | key | |
---|---|---|
0 | 0 | a |
1 | 1 | b |
2 | 2 | d |
這是一種多對一的合併。df1中的資料有多個被標記為a和b的行,而df2中key列的每個值則僅對應一行。對這些物件呼叫merge即可得到:
pd.merge(df1,df2)
data1 | key | data2 | |
---|---|---|---|
0 | 0 | b | 1 |
1 | 1 | b | 1 |
2 | 6 | b | 1 |
3 | 2 | a | 0 |
4 | 4 | a | 0 |
5 | 5 | a | 0 |
注意,我並沒有指明要用哪個列進行連線。如果沒有指定,merge就會將重疊列的列名當做鍵。不過,最好明確指定一下:
pd.merge(df1,df2,on='key')
data1 | key | data2 | |
---|---|---|---|
0 | 0 | b | 1 |
1 | 1 | b | 1 |
2 | 6 | b | 1 |
3 | 2 | a | 0 |
4 | 4 | a | 0 |
5 | 5 | a | 0 |
如果兩個物件的列名不同,也可以分別進行指定:
df3 = pd.DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],'data1': range(7)})
df4 = pd.DataFrame({'rkey': ['a', 'b', 'd'],'data2': range(3)})
df3
data1 | lkey | |
---|---|---|
0 | 0 | b |
1 | 1 | b |
2 | 2 | a |
3 | 3 | c |
4 | 4 | a |
5 | 5 | a |
6 | 6 | b |
df4
data2 | rkey | |
---|---|---|
0 | 0 | a |
1 | 1 | b |
2 | 2 | d |
pd.merge(df3,df4,left_on='lkey',right_on='rkey')
data1 | lkey | data2 | rkey | |
---|---|---|---|---|
0 | 0 | b | 1 | b |
1 | 1 | b | 1 | b |
2 | 6 | b | 1 | b |
3 | 2 | a | 0 | a |
4 | 4 | a | 0 | a |
5 | 5 | a | 0 | a |
可能你已經注意到了,結果裡面c和d以及與之相關的資料消失了。預設情況下,merge做的是“內連線”;結果中的鍵是交集。其他方式還有"left"、“right"以及"outer”。外連線求取的是鍵的並集,組合了左連線和右連線的效果:
pd.merge(df1,df2,how='outer')
data1 | key | data2 | |
---|---|---|---|
0 | 0.0 | b | 1.0 |
1 | 1.0 | b | 1.0 |
2 | 6.0 | b | 1.0 |
3 | 2.0 | a | 0.0 |
4 | 4.0 | a | 0.0 |
5 | 5.0 | a | 0.0 |
6 | 3.0 | c | NaN |
7 | NaN | d | 2.0 |
表8-1對這些選項進行了總結。
選項 | 說明 |
---|---|
inner | 使用兩個表都有的鍵 |
left | 使用左表中所有的鍵 |
right | 使用右表中所有的鍵 |
outer | 使用兩個表中所有的鍵 |
多對多的合併有些不直觀。看下面的例子:
df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],'data1': range(6)})
df2 = pd.DataFrame({'key': ['a', 'b', 'a', 'b', 'd'], 'data2': range(5)})
df1
data1 | key | |
---|---|---|
0 | 0 | b |
1 | 1 | b |
2 | 2 | a |
3 | 3 | c |
4 | 4 | a |
5 | 5 | b |
df2
data2 | key | |
---|---|---|
0 | 0 | a |
1 | 1 | b |
2 | 2 | a |
3 | 3 | b |
4 | 4 | d |
pd.merge(df1,df2,on='key',how='left')
data1 | key | data2 | |
---|---|---|---|
0 | 0 | b | 1.0 |
1 | 0 | b | 3.0 |
2 | 1 | b | 1.0 |
3 | 1 | b | 3.0 |
4 | 2 | a | 0.0 |
5 | 2 | a | 2.0 |
6 | 3 | c | NaN |
7 | 4 | a | 0.0 |
8 | 4 | a | 2.0 |
9 | 5 | b | 1.0 |
10 | 5 | b | 3.0 |
多對多連線產生的是行的笛卡爾積。由於左邊的DataFrame有3個"b"行,右邊的有2個,所以最終結果中就有6個"b"行。連線方式隻影響出現在結果中的不同的鍵的值:
pd.merge(df1,df2,on='key',how='inner')
data1 | key | data2 | |
---|---|---|---|
0 | 0 | b | 1 |
1 | 0 | b | 3 |
2 | 1 | b | 1 |
3 | 1 | b | 3 |
4 | 5 | b | 1 |
5 | 5 | b | 3 |
6 | 2 | a | 0 |
7 | 2 | a | 2 |
8 | 4 | a | 0 |
9 | 4 | a | 2 |
要根據多個鍵進行合併,傳入一個由列名組成的列表即可:
left = pd.DataFrame({'key1': ['foo', 'foo', 'bar'],'key2': ['one', 'two', 'one'],'lval': [1, 2, 3]})
right = pd.DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'], 'key2': ['one', 'one', 'one', 'two'], 'rval': [4, 5, 6, 7]})
left
key1 | key2 | lval | |
---|---|---|---|
0 | foo | one | 1 |
1 | foo | two | 2 |
2 | bar | one | 3 |
right
key1 | key2 | rval | |
---|---|---|---|
0 | foo | one | 4 |
1 | foo | one | 5 |
2 | bar | one | 6 |
3 | bar | two | 7 |
pd.merge(left,right,on=['key1','key2'],how='outer')
key1 | key2 | lval | rval | |
---|---|---|---|---|
0 | foo | one | 1.0 | 4.0 |
1 | foo | one | 1.0 | 5.0 |
2 | foo | two | 2.0 | NaN |
3 | bar | one | 3.0 | 6.0 |
4 | bar | two | NaN | 7.0 |
結果中會出現哪些鍵組合取決於所選的合併方式,你可以這樣來理解:多個鍵形成一系列元組,並將其當做單個連線鍵(當然,實際上並不是這麼回事)。
注意:在進行列-列連線時,DataFrame物件中的索引會被丟棄。
對於合併運算需要考慮的最後一個問題是對重複列名的處理。雖然你可以手工處理列名重疊的問題(檢視前面介紹的重新命名軸標籤),但merge有一個更實用的suffixes選項,用於指定附加到左右兩個DataFrame物件的重疊列名上的字串:
pd.merge(left,right,on='key1')
key1 | key2_x | lval | key2_y | rval | |
---|---|---|---|---|---|
0 | foo | one | 1 | one | 4 |
1 | foo | one | 1 | one | 5 |
2 | foo | two | 2 | one | 4 |
3 | foo | two | 2 | one | 5 |
4 | bar | one | 3 | one | 6 |
5 | bar | one | 3 | two | 7 |
pd.merge(left,right,on='key1',suffixes=['_left','_right'])
key1 | key2_left | lval | key2_right | rval | |
---|---|---|---|---|---|
0 | foo | one | 1 | one | 4 |
1 | foo | one | 1 | one | 5 |
2 | foo | two | 2 | one | 4 |
3 | foo | two | 2 | one | 5 |
4 | bar | one | 3 | one | 6 |
5 | bar | one | 3 | two | 7 |
merge的引數請參見表8-2。使用DataFrame的行索引合併是下一節的主題。
引數 | 說明 |
---|---|
left | 參與合併的左側 Data Frame |
right | 參與合併的右側 Data Frame |
how | "inner","outer"、"left"、 "right"其中之ー。認為inner |
on | 用於連線的列名。必須存在於左右兩個 Data Frame物件中。如果未指定,且其他連線鍵也未指定,則以left和right列名的交集作為連線鍵 |
left_on | 左側 Data Frame中用作連線健的列 |
right_on | 右側 Dataframe r中用作連線鍵的列 |
left_index | 將左側的行索引用作其連線鍵 |
right_index | 類似於 left_ndex |
sort | 根據連線健對合並後的資料進行排序,預設為True,有時在處理大資料集時,禁用該選項可獲得更好的效能 |
suffixes | 字串值元組,用於追加到重複列名的末尾,預設為(’_x’,’_y’)。例如,如果左右兩個 Dataframe物件都有"data”,則結果中就會出現’data_x’和’data_y’ |
copy | 設定為 False,可以在某些特殊情況下避免將資料複製到結果資料結構中。預設總是複製 |
indicator | 新增特殊的列_merge,它可以指明每個行的來源,它的值有left_only、right_only或both,根據每行的合併資料的來源。 |
索引上的合併
有時候,DataFrame中的連線鍵位於其索引中。在這種情況下,你可以傳入left_index=True或right_index=True(或兩個都傳)以說明索引應該被用作連線鍵:
left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'],'value': range(6)})
right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
left1
key | value | |
---|---|---|
0 | a | 0 |
1 | b | 1 |
2 | a | 2 |
3 | a | 3 |
4 | b | 4 |
5 | c | 5 |