數據分析之Pandas合併操作總結

↑↑↑關注後"星標"Datawhale每日干貨 & 每月組隊學習,不錯過
 Datawhale乾貨 
作者:耿遠昊,Datawhale成員,華東師範大學
pandas 是一個強大的分析結構化數據的工具集,它的使用基礎是Numpy(提供高性能的矩陣運算),用於數據挖掘和數據分析,同時也提供數據清洗功能。Pandas做分析數據,可以分爲索引、分組、變形及合併四種操作。前邊已經介紹過索引操作、分組操作及變形操作,最後對Pandas中的合併操作進行介紹,涉及知識點提綱如下圖:     本文目錄
              1. append與assign                  1.1. append方法                  1.2. assign方法2. combine與update    2.1. combine方法    2.2. update方法              3. concat方法                4. merge與join    4.1. merge函數    4.2. join函數在詳細講解每個模塊之前,首先讀入數據:
import numpy as np
import pandas as pd
df = pd.read_csv('joyful-pandas-master/data/table.csv')
df.head()

append與assign

1. append方法(一般用來添加行)

(1)利用序列添加行(必須指定name)

df_append = df.loc[:3,['Gender','Height']].copy()  # 默認深拷貝
df_append

s = pd.Series({'Gender':'F','Height':188},name='new_row')  # name定義增加的這一行索引名。
df_append.append(s)

(2)用DataFrame添加表

df_temp = pd.DataFrame({'Gender':['F','M'],'Height':[188,176]})
df_append.append(df_temp)

從上面可以看到這個索引是不會自動往下續的,因爲我們新建的df_temp如下:

pd.DataFrame({'Gender':['F','M'],'Height':[188,176]})

可以看到這個索引就是0和1,如果你直接append而不加參數則就會直接將上面的DataFrame直接和df_append粘在一起而不會改變索引,那麼怎麼改變索引使得這個索引順着前面的索引呢?看下面的例子:

df_append.append(df_temp, ignore_index=True)

下面是這個append函數的原形式:

DataFrame.append(self,other,ignore_index=False,verify_integrity=False,sort=False)

其中的ignore_index就是表示是否要跟着前面的索引來定義後面的索引,一般來說是默認False,也就是像我們的第一個例子這樣。現在我們將這個參數改成True,就可以順着索引了,就像上面的這個例子一樣。

當然這裏也可以自行改變索引名:

df_temp = pd.DataFrame({'Gender':['F','M'],'Height':[188,176]},index=['new_1','new_2'])
df_append.append(df_temp)

其他的參數學習可以參考這個網站:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.append.html?highlight=append#pandas.DataFrame.append

2. assign方法(一般用來添加列)

該方法主要用於添加列,列名直接由參數指定:

s = pd.Series(list('abcd'),index=range(4))
df_append.assign(Letter=s)  # 這裏定義列名就直接在assign參數定義。

這個一般定義要添加的列Series是沒有列索引名的:

s = pd.Series(list('abcd'),index=range(4))
s

可以一次添加多個列:

df_append.assign(col1=lambda x:x['Gender']*2, col2=s)

可以看出這個可以添加任意多個列,但是都是要在參數中依次定義的。

參考學習:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.assign.html?highlight=assign#pandas.DataFrame.assign

combine與update

1. combine方法

combine和update都是用於表的填充函數,可以根據某種規則填充。

(1)填充對象

可以看出combine方法是按照表的順序輪流進行逐列循環的,而且自動索引對齊,缺失值爲NaN,理解這一點很重要。

df_combine_1 = df.loc[:1,['Gender','Height']].copy()
df_combine_2 = df.loc[10:11,['Gender','Height']].copy()
df_combine_1.combine(df_combine_2,lambda x,y:print(x))

因爲lambda函數是輸出x和y,沒有返回值所以都爲NaN。

df1 = pd.DataFrame({'A': [5, 0], 'B': [2, 4]})
df2 = pd.DataFrame({'A': [1, 1], 'B': [3, 3]})
df1.combine(df2, np.minimum)

combine函數原型:

DataFrame.combine(self,other:'DataFrame',func,fill_value = None,overwrite = True)

這裏通過多個例子嘗試可以發現,func函數是必不可少的,也就是我們必須有一個func來返回數值。

(2)一些例子

例①:根據列均值的大小填充

df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [8, 7], 'B': [6, 5]})
df1.combine(df2,lambda x,y:x if x.mean()>y.mean() else y)

例②:索引對齊特性(默認狀態下,後面的表沒有的行列都會設置爲NaN)

df2 = pd.DataFrame({'B': [8, 7], 'C': [6, 5]},index=[1,2])
df1.combine(df2,lambda x,y:x if x.mean()>y.mean() else y)

例③:使得df1原來符合條件的值不會被覆蓋

df1.combine(df2,lambda x,y:x if x.mean()>y.mean() else y,overwrite=False)

例④:在新增匹配df2的元素位置填充-1

df1.combine(df2,lambda x,y:x if x.mean()>y.mean() else y,fill_value=-1)
# 也就是將NaN位置補成-1

參考學習:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.combine.html?highlight=combine#pandas.DataFrame.combine

(3)combine_first方法

這個方法作用是用df2填補df1的缺失值,功能比較簡單,但很多時候會比combine更常用,下面舉兩個例子:

df1 = pd.DataFrame({'A': [None, 0], 'B': [None, 4]})
df2 = pd.DataFrame({'A': [1, 1], 'B': [3, 3]})
df1.combine_first(df2)

也就是要在df1的基礎之上,如果df1有缺失值,就在df2的對應位置補上去,當然如果df1沒有缺失值,則這個填充也就相當於沒填充,也就意義不大了。

df1 = pd.DataFrame({'A': [None, 0], 'B': [4, None]})
df2 = pd.DataFrame({'B': [3, 3], 'C': [1, 1]}, index=[1, 2])
df1.combine_first(df2)

當然,如果df1的缺失值位置在df2中也是NaN,那也是不會填充的。

這裏也涉及到很多參數問題,可以參考這個:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.combine_first.html#pandas.DataFrame.combine_first

2. update方法

(1)三個特點

①返回的框索引只會與被調用框的一致(默認使用左連接,下一節會介紹)

②第二個框中的nan元素不會起作用

③沒有返回值,直接在df上操作

(2)例子

例①:索引完全對齊情況下的操作

df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [400, 500, 600]})
df2 = pd.DataFrame({'B': [4, 5, 6], 'C': [7, 8, 9]})
df1.update(df2)
df1

這裏需要注意:這個也是在df1的基礎之上進行改變,而這個update是連行列索引都不改變,不增加,就是在這個基礎上,對df1中對應位置的元素改成df2中對應位置的元素。看上面的例子也很好理解了。


例②:部分填充

df1 = pd.DataFrame({'A': ['a', 'b', 'c'], 'B': ['x', 'y', 'z']})
df2 = pd.DataFrame({'B': ['d', 'e']}, index=[1,2])
df1.update(df2)
df1

這個例子就是說明了,我們這個操作可以對df1的某幾個元素進行改變,不一定是要整行整列改變。


例③:缺失值不會填充

df1 = pd.DataFrame({'A': [1, 2, 3], 'B': [400, 500, 600]})
df2 = pd.DataFrame({'B': [4, np.nan, 6]})
df1.update(df2)
df1

這個例子就是,我們如果update了缺失值NaN,則就不會在原df1中把對應元素改成NaN了,這個缺失值是不會被填充的。

更多參數參考:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.update.html?highlight=update#pandas.DataFrame.update

concat方法

concat方法可以在兩個維度上拼接,默認縱向憑藉(axis=0),拼接方式默認外連接

所謂外連接,就是取拼接方向的並集,而'inner'時取拼接方向(若使用默認的縱向拼接,則爲列的交集)的交集

下面舉一些例子說明其參數:

df1 = pd.DataFrame({'A': ['A0', 'A1'], 'B': ['B0', 'B1']}, index = [0,1])
df2 = pd.DataFrame({'A': ['A2', 'A3'], 'B': ['B2', 'B3']}, index = [2,3])
df3 = pd.DataFrame({'A': ['A1', 'A3'], 'D': ['D1', 'D3'], 'E': ['E1', 'E3']},index = [1,3])
df1

df2

df3

默認狀態拼接:

pd.concat([df1,df2])

axis=1時沿列方向拼接:

pd.concat([df1,df2],axis=1)

join設置爲內連接(由於axis=0,因此列取交集):

pd.concat([df3,df1])

pd.concat([df3,df1],join='inner')  # 對索引取交集

join設置爲外鏈接:

pd.concat([df3,df1],join='outer')

pd.concat([df3,df1],join='outer',sort=True) #sort設置列排序,默認爲False

其實就是對列索引進行排序。verify_integrity檢查列是否唯一:
pd.concat([df2,df1],verify_integrity=True,sort=True)
# pd.concat([df3,df1],verify_integrity=True,sort=True) 報錯

這裏因爲df1和df2的列索引相同,所以可以正常返回。而df1和df3的列索引不同,所以會報錯。

這個verify_integrity就是爲了保證只有在索引相同時纔會進行操作的函數,而可以拿來檢查函數列是否唯一。同樣,可以添加Series:

s = pd.Series(['X0', 'X1'], name='X')
pd.concat([df1,s],axis=1)

key參數用於對不同的數據框增加一個標號,便於索引:

pd.concat([df1,df2], keys=['x', 'y'])

pd.concat([df1,df2], keys=['x', 'y']).index

這裏相當於對原索引的基礎上,又設定了行索引,針對這個df1和df2。然這裏也可以解決行索引雜亂無章的問題,和append一樣,都是通過ignore_index參數來完成:

pd.concat([df3,df1], ignore_index=True)

更多參數參考:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.concat.html?highlight=concat#pandas.concat

merge與join

1. merge函數

merge函數的作用是將兩個pandas對象橫向合併,遇到重複的索引項時會使用笛卡爾積,默認inner連接,可選left、outer、right連接。

所謂左連接,就是指以第一個表索引爲基準,右邊的表中如果不再左邊的則不加入,如果在左邊的就以笛卡爾積的方式加入。

merge/join與concat的不同之處在於on參數,可以指定某一個對象爲key來進行連接

同樣的,下面舉一些例子:

left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'], 'key2': ['K0', 'K1', 'K0', 'K1'],
                      'A': ['A0', 'A1', 'A2', 'A3'], 'B': ['B0', 'B1', 'B2', 'B3']})
right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],'key2': ['K0', 'K0', 'K0', 'K0'],
                      'C': ['C0', 'C1', 'C2', 'C3'],'D': ['D0', 'D1', 'D2', 'D3']})
right2 = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],'key2': ['K0', 'K0', 'K0', 'K0'],
                      'C': ['C0', 'C1', 'C2', 'C3']})
left

right

right2

以key1爲準則連接,如果具有相同的列,則默認suffixes=('_x','_y'):

pd.merge(left, right, on='key1')

這個函數相對有點複雜,可以多看例子來想想。以多組鍵連接:

pd.merge(left, right, on=['key1','key2'])

默認使用inner連接,因爲merge只能橫向拼接,所以取行向上keys的交集,下面看如果使用how=outer參數。注意:這裏的how就是concat的join

pd.merge(left, right, how='outer', on=['key1','key2'])

使用了how='outer',那麼如果行中帶有缺失值也會被返回。

左連接:

pd.merge(left, right, how='left', on=['key1', 'key2'])

以左邊的left表的索引爲基準。

右連接:

pd.merge(left, right, how='right', on=['key1', 'key2'])

以右邊的right表索引爲基準。

如果還是對笛卡爾積不太瞭解,請務必理解下面這個例子,由於B的所有元素爲2,因此需要6行:

left = pd.DataFrame({'A': [1, 2], 'B': [2, 2]})
right = pd.DataFrame({'A': [4, 5, 6], 'B': [2, 2, 2]})
print(left)
print(right)

pd.merge(left, right, on='B', how='outer')

validate檢驗的是到底哪一邊出現了重複索引,如果是“one_to_one”則兩側索引都是唯一,如果"one_to_many"則左側唯一

left = pd.DataFrame({'A': [1, 2], 'B': [2, 2]})
right = pd.DataFrame({'A': [4, 5, 6], 'B': [2, 3, 4]})
print(left)
print(right)

# pd.merge(left, right, on='B', how='outer',validate='one_to_one')  # 報錯
# MergeError: Merge keys are not unique in left dataset; not a one-to-one merge

這裏因爲left中索引不唯一,所以報錯了。所以我們改一下left,使得它索引唯一。如下例:

left = pd.DataFrame({'A': [1, 2], 'B': [2, 1]})
pd.merge(left, right, on='B', how='outer',validate='one_to_one')

indicator參數指示了,合併後該行索引的來源

df1 = pd.DataFrame({'col1': [0, 1], 'col_left': ['a', 'b']})
df2 = pd.DataFrame({'col1': [1, 2, 2], 'col_right': [2, 2, 2]})
pd.merge(df1, df2, on='col1', how='outer', indicator=True)  # indicator='indicator_column'也是可以的

這裏就是新增一列表明每行索引的來源。

更多參數:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.merge.html?highlight=merge#pandas.DataFrame.merge

2. join函數

join函數作用是將多個pandas對象橫向拼接,遇到重複的索引項時會使用笛卡爾積,默認左連接,可選inner、outer、right連接。

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'], 'B': ['B0', 'B1', 'B2']}, index=['K0', 'K1', 'K2'])
right = pd.DataFrame({'C': ['C0', 'C2', 'C3'], 'D': ['D0', 'D2', 'D3']}, index=['K0', 'K2', 'K3'])
left.join(right)

這裏是默認左連接,也就是按照left索引的基礎上來填充。對於many_to_one模式下的合併,往往join更爲方便。同樣可以指定key:

left = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'], 'B': ['B0', 'B1', 'B2', 'B3'], 'key': ['K0', 'K1', 'K0', 'K1']})
right = pd.DataFrame({'C': ['C0', 'C1'], 'D': ['D0', 'D1']}, index=['K0', 'K1'])
print(left)
print(right)

left.join(right, on='key')

多層key:

left = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3'],
                     'key1': ['K0', 'K0', 'K1', 'K2'],
                     'key2': ['K0', 'K1', 'K0', 'K1']})
index = pd.MultiIndex.from_tuples([('K0', 'K0'), ('K1', 'K0'),
                                   ('K2', 'K0'), ('K2', 'K1')],names=['key1','key2'])
right = pd.DataFrame({'C': ['C0', 'C1', 'C2', 'C3'],
                      'D': ['D0', 'D1', 'D2', 'D3']},
                     index=index)
print(left)
print(index)
print(right)

left.join(right, on=['key1','key2'])

參考:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.join.html?highlight=join#pandas.DataFrame.join

問題與練習

1. 問題

【問題一】請思考什麼是append/assign/combine/update/concat/merge/join各自最適合使用的場景,並舉出相應的例子。

  • append:主要是用來添加行,也就是在一個表中下方添加。

  • assign:主要是用來添加列,也就是在表的右方添加。

  • combine:這個函數的填充可以根據某種規則來填充,當然它衍生的combine_first就是一個比較常用的函數了,這個函數是直接填充。

  • update:這個函數是會在前表的基礎之上,將後表填充,不會更改索引,也就是按照前表的索引來操作。

  • concat:這個函數也是進行直接的拼接,不會管索引,所以會出現多個相同的索引的情況,主要用於列的拼接。

  • merge:這個函數就是用於行拼接多一些,可以指定key來拼接,多用於one_to_one和one_to_many的情況。

  • join:這個函數也適用於行拼接,多用於many_to_one的情況,還可以應對多層keys的拼接。

例子的話可以看上面的講解,也是比較詳細的。

【問題二】merge_ordered和merge_asof的作用是什麼?和merge是什麼關係?

作用可以參考:

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.merge_ordered.html?highlight=merge_ordered#pandas.merge_ordered https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.merge_asof.html#pandas.merge_asof

應該是merge的衍生出來的函數,可以完善merge函數的一切缺陷。

【問題三】請構造一個多級索引與多級索引合併的例子,嘗試使用不同的合併函數。

下面建立兩個多級索引。

df1=pd.DataFrame(np.arange(12).reshape(4,3),index=[list("AABB"),[1,2,1,2]],columns=[list("XXY"),[10,11,10]])
df1

df2=pd.DataFrame(np.arange(9).reshape(3,3),index=[list("CCD"),[3,3,4]],columns=[list("ZZK"),[9,7,9]])
df2

下面是幾個合併的例子。

df1.append(df2)

df1.combine_first(df2)

df2.update(df1)
df2

pd.concat([df1,df2])

【問題四】上文提到了連接的笛卡爾積,那麼當連接方式變化時(inner/outer/left/right),這種笛卡爾積規則會相應變化嗎?請構造相應例子。

答:就是我們用merge的時候,他會自動計算笛卡爾積,但是最後返回的是不是全部的笛卡爾積,就要看這些連接方式了,有時候是左連接,那就會根據左表的索引來返回,有時候右連接,就會根據右表索引來返回,有時候也會全部返回,這些就要看參數了。

2. 練習

【練習一】有2張公司的員工信息表,每個公司共有16名員工,共有五個公司,請解決如下問題:

pd.read_csv('joyful-pandas-master/data/Employee1.csv').head()

pd.read_csv('joyful-pandas-master/data/Employee2.csv').head()

(a) 每個公司有多少員工滿足如下條件:既出現第一張表,又出現在第二張表。

df1=pd.read_csv('joyful-pandas-master/data/Employee1.csv')
df2=pd.read_csv('joyful-pandas-master/data/Employee2.csv')
df1.head()

df2.head()

pd.merge(df1['Name'],df2['Name'])

(b) 將所有不符合(a)中條件的行篩選出來,合併爲一張新表,列名與原表一致。

L = list(set(df1['Name']).interp(set(df2['Name'])))
df_b1 = df1[~df1['Name'].isin(L)]
df_b2 = df2[~df2['Name'].isin(L)]
df_b = pd.concat([df_b1,df_b2]).set_index('Name')
df_b.head()

(c) 現在需要編制所有80位員工的信息表,對於(b)中的員工要求不變,對於滿足(a)條件員工,它們在某個指標的數值,取偏離它所屬公司中滿足(b)員工的均值數較小的哪一個,例如:P公司在兩張表的交集爲{p1},並集扣除交集爲{p2,p3,p4},那麼如果後者集合的工資均值爲1萬元,且p1在表1的工資爲13000元,在表2的工資爲9000元,那麼應該最後取9000元作爲p1的工資,最後對於沒有信息的員工,利用缺失值填充。

df1['重複'] = ['Y_1' if df1.loc[i,'Name'] in L else 'N' for i in range(df1.shape[0])]
df2['重複'] = ['Y_2' if df2.loc[i,'Name'] in L else 'N' for i in range(df2.shape[0])]
df1 = df1.set_index(['Name','重複'])
df2 = df2.set_index(['Name','重複'])
df_c = pd.concat([df1,df2])
result = pd.DataFrame({'Company':[],'Name':[],'Age':[],'Height':[],'Weight':[],'Salary':[]})
group = df_c.groupby(['Company','重複'])
for i in L:
    first = group.get_group((i[0].upper(),'Y_1')).reset_index(level=1).loc[i,:][-4:]
    second = group.get_group((i[0].upper(),'Y_2')).reset_index(level=1).loc[i,:][-4:]
    mean = group.get_group((i[0].upper(),'N')).reset_index(level=1).mean()
    final = [i[0].upper(),i]
    for j in range(4):
        final.append(first[j] if abs(first[j]-mean[j])<abs(second[j]-mean[j]) else second[j])
    result = pd.concat([result,pd.DataFrame({result.columns.tolist()[k]:[final[k]] for k in range(6)})])
result = pd.concat([result.set_index('Name'),df_b])
for i in list('abcde'):
    for j in range(1,17):
        item = i+str(j)
        if item not in result.index:
            result = pd.concat([result,pd.DataFrame({'Company':[i.upper()],'Name':[item]
                 ,'Age':[np.nan],'Height':[np.nan],'Weight':[np.nan],'Salary':[np.nan]}).set_index('Name')])
result['Number'] = [int(i[1:]) for i in result.index]
result.reset_index().drop(columns='Name').set_index(['Company','Number']).sort_index()

【練習二】有2張課程的分數表(分數隨機生成),但專業課(學科基礎課、專業必修課、專業選修課)與其他課程混在一起,請解決如下問題:

pd.read_csv('joyful-pandas-master/data/Course1.csv').head()

pd.read_csv('joyful-pandas-master/data/Course2.csv').head()

(a) 將兩張表分別拆分爲專業課與非專業課(結果爲四張表)。

df1=pd.read_csv('joyful-pandas-master/data/Course1.csv')
df1.head()

df2=pd.read_csv('joyful-pandas-master/data/Course2.csv')
df2.head()

df_a11= df1.query('課程類別 in ["學科基礎課","專業必修課","專業選修課"]')
df_a12= df1.query('課程類別 not in ["學科基礎課","專業必修課","專業選修課"]')
df_a21= df2.query('課程類別 in ["學科基礎課","專業必修課","專業選修課"]')
df_a22= df2.query('課程類別 not in ["學科基礎課","專業必修課","專業選修課"]')
df_a11.head()

df_a12.head()

df_a21.head()

df_a22.head()

(b) 將兩張專業課的分數表和兩張非專業課的分數表分別合併。

df_a11.append(df_a21, ignore_index=True)  # 專業課

df_a12.append(df_a22, ignore_index=True)  # 非專業課

(c) 不使用(a)中的步驟,請直接讀取兩張表合併後拆分。

df = pd.concat([df1,df2])
df

special = df.query('課程類別 in ["學科基礎課","專業必修課","專業選修課"]')
common = df.query('課程類別 not in ["學科基礎課","專業必修課","專業選修課"]')
special.head()
common.head()

(d) 專業課程中有缺失值嗎,如果有的話請在完成(3)的同時,用組內(3種類型的專業課)均值填充缺失值後拆分。

df.isnull().any()

說明“分數”列是存在缺失值的,所以我們需要將“分數”列的缺失值補上。

df['分數'] = df.groupby('課程類別').transform(lambda x: x.fillna(x.mean()))['分數']
df['分數'].isnull().any()
False

出現False說明缺失值已經被填補完成了。下一步就開始拆分:

special2 = df.query('課程類別 in ["學科基礎課","專業必修課","專業選修課"]')
common2 = df.query('課程類別 not in ["學科基礎課","專業必修課","專業選修課"]')
special2.head()

common2.head()

本文電子版 後臺回覆 合併 獲取

“感謝你的在看,點贊,分享三

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章